Sum Index Examples
This chapter walks through a representative contract and shows what a sum-query proof actually proves — both the path query the prover signs and the verified element the verifier extracts. Every example uses the same tip document type on the tip-jar contract at packages/rs-drive/tests/supporting_files/contract/tip-jar/tip-jar-contract.json, so the proof bytes, verified elements, and diagrams can all be cross-referenced against the same data once the bench fixture lands.
The chapter assumes you've read Document Sum Trees — that chapter explains the three tree variants (NormalTree / SumTree / ProvableSumTree), how Element::NonCounted-style "doesn't contribute to my parent's aggregation" wrappers work (now for sums as well), and how the schema's documentsSummable / rangeSummable flags select between them. Here we take that machinery as given and trace what each query sees.
Status: the bench at
packages/rs-drive/benches/document_sum_worst_case.rslands the reproducible numbers below — same convention as the Count Index Examples chapter. All proof sizes are measured against a 100 000-row fixture; verifiedsumvalues are the actual sums the bench's matrix reports. The full surface — primary-key total, point lookups, In-fan-out,AggregateSumOnRangeon both top-level and compound indexes, and the carrier-aggregate primitive from grovedb PR #670 — is fully wired and producing the byte counts in the table below.
The Tip Jar Contract
The tip document type carries four properties (recipient, amount, sentAt, note), opts into total sums at the doctype level via documentsSummable: "amount", and declares three indexes covering the sum-query surface:
{
"type": "object",
"documentsMutable": false,
"documentsSummable": "amount",
"properties": {
"recipient": { "type": "array", "byteArray": true, "minItems": 32, "maxItems": 32,
"position": 0, "contentMediaType": "application/x.dash.dpp.identifier" },
"amount": { "type": "integer", "minimum": 1, "position": 1 },
"sentAt": { "type": "integer", "minimum": 0, "position": 2 },
"note": { "type": "string", "maxLength": 280, "position": 3 }
},
"required": ["recipient", "amount", "sentAt"],
"indices": [
{
"name": "byRecipient",
"properties": [{ "recipient": "asc" }],
"summable": "amount"
},
{
"name": "bySentAt",
"properties": [{ "sentAt": "asc" }],
"summable": "amount",
"rangeSummable": true
},
{
"name": "byRecipientTime",
"properties": [{ "recipient": "asc" }, { "sentAt": "asc" }],
"summable": "amount",
"rangeSummable": true
}
],
"additionalProperties": false
}
Three things to notice:
documentsSummable: "amount"at the document-type level upgrades the doctype's primary-key subtree (attip/[0]) fromNormalTreetoSumTree. The unfiltered total sum is one read against this element'ssum_value. The string-form value names the property each insert contributes to the tree — the picker uses it to validate that any futureGetDocumentsSumrequest whosesum_propertydoesn't match"amount"is rejected at parse time.byRecipientissummable: "amount"only. It doesn't opt intorangeSummable, sorecipient > Xrange sums aren't supported. Every summable terminator's value tree is stored as aSumTreeregardless ofrangeSummable, so point-lookup sum proofs (e.g.recipient == Xorrecipient IN [...]) get a compact value-tree-direct shape.rangeSummableis strictly an opt-in forAggregateSumOnRangesupport — orthogonal to proof-size shape on point queries.bySentAtandbyRecipientTimearerangeSummable: true. Their property-name subtrees (e.g.tip/sentAt) are stored asProvableSumTreerather thanNormalTree, which is whatAggregateSumOnRangewalks forsentAt > floorstyle queries.
The bench populates 100 000 tips under a deterministic schedule: row → (recipient_(row % 100), sentAt = row, amount = (row % 10) + 1). That gives exactly 1 000 tips per recipient, but with an asymmetry worth flagging: since both recipient and amount are derived from row modulo (100 and 10 respectively), each recipient sees only one amount value across all their 1 000 tips. Recipient n always has amount = (n % 10) + 1:
recipient_000,recipient_010, …,recipient_090→amount = 1, per-recipient sum = 1 000recipient_001,recipient_011, …,recipient_091→amount = 2, per-recipient sum = 2 000- …
recipient_009,recipient_019, …,recipient_099→amount = 10, per-recipient sum = 10 000
The headline numbers:
- Total
sum(amount)across all tips: 550 000 (each amount value 1..10 appears 10 000 times →10 000 × 55). The primary-keydocumentsSummableSumTree reports this directly via Query 1's O(1) read. sum(amount)per recipient: varies 1 000–10 000 by recipient (see the cycle above).sum(amount)for any contiguoussentAtrange of length 10: exactly 55 (every 10-row window covers one full cycle of1..10).sum(amount)for the first half of the timeline (sentAt < 50 000): 275 000.
Those numbers appear in every verified-element block 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 sum_value committed to the merk root; yellow nodes are ProvableSumTree (per-node sums); gray are regular subtrees; dashed boxes highlight Element::NonCounted-style wrappers (children that store data but contribute 0 to their parent's aggregation).
flowchart TB
TD["@/contract_id/0x01/tip"]:::tree
TD --> PK["[0]: SumTree sum=550000<br/>(documentsSummable primary key)"]:::sumnode
TD --> RC["recipient: NormalTree<br/>(byRecipient property-name)"]:::node
TD --> SA["sentAt: ProvableSumTree<br/>(bySentAt property-name)"]:::pstnode
RC --> R000["recipient_000: SumTree sum=1000<br/>(amount cycle: 1)"]:::sumnode
RC --> R050["recipient_050: SumTree sum=1000<br/>(amount cycle: 1)"]:::sumnode
RC --> RMore["... recipient_001 sum=2000 ... recipient_009 sum=10000<br/>(per row%10 amount cycle)"]:::sumnode
R050 --> R050_0["[0]: SumTree sum=1000<br/>(byRecipient refs)"]:::sumnode
R050 --> R050_S["sentAt: NonCounted(ProvableSumTree)<br/>(byRecipientTime continuation, contributes 0)"]:::noncounted
R050_S --> R050_S_500["sentAt_00050000: SumTree sum=1"]:::sumnode
R050_S_500 --> R050_S_500_0["[0]: SumTree sum=1<br/>(byRecipientTime ref, amount=1)"]:::sumnode
SA --> S500["sentAt_00050000: SumTree sum=1<br/>(bySentAt terminator, amount=1)"]:::sumnode
SA --> SMore["... sentAt_00000000 ... sentAt_00099999"]:::sumnode
S500 --> S500_0["[0]: SumTree sum=1<br/>(bySentAt ref)"]:::sumnode
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef node fill:#6e7681,color:#fff,stroke:#6e7681;
classDef sumnode fill:#3fb950,color:#0d1117,stroke:#3fb950,stroke-width:2px;
classDef pstnode 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:
recipient_050is aSumTreewithsum_value = 1000. That's true becausebyRecipientis summable; the rule applies uniformly to every summable tier. ThesentAtcontinuation that branches off this value tree isNonCounted-wrapped so the parent's sum equals exactly the contribution in[0](which for recipient_050 is1 000 × 1 = 1 000per the amount cycle). Other recipients get different value-tree sums per the per-recipient amount described above.tip/sentAtis aProvableSumTree, not a regularNormalTree. The yellow class above marks that — each internal merk node carries its subtree's sum, which is what makesAggregateSumOnRangea single-pass primitive.sentAt_00050000is aSumTreewithsum_value = 1under either parent (the globalbySentAtpath or the per-recipientbyRecipientTimecontinuation). Same element layout under both; 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;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_sum_queryfor the range primitive) returns after walking the proof bytes. Thesum_value_or_defaultfield on aSumTreeelement is what the sum surface ultimately surfaces to the caller. - Proof display — the proof bytes decoded via
bincodeinto the structuredGroveDBProofAST and rendered through itsDisplayimpl, same convention as the count chapter. The bench'sdisplay_proofsblock emits these inline; reproduce locally withDASH_PLATFORM_SUM_BENCH_REBUILD=1 cargo bench -p drive --bench document_sum_worst_case -- --test 2>&1 | grep -A 200 "^\[display\]". - 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.
Proof-size numbers below come from the 100 000-row bench run on the byRecipient / bySentAt / byRecipientTime indexes. Avg-time numbers are median-of-5 wall-clock measurements with one warmup discarded (see the methodology note under the queries table).
Queries in this Chapter
| # | Query | Filter | Complexity | Avg time | Proof size |
|---|---|---|---|---|---|
| 1 | Unfiltered Total Sum | (none — total at doctype level) | O(1) | 23.6 µs | 580 B |
| 2 | Equal on a Single Property (byRecipient) | recipient == "recipient_050" | O(log R) | 37.2 µs | 1 087 B |
| 3 | Equal on a RangeSummable Property (bySentAt) | sentAt == 50000 | O(log T) | 72.1 µs | 1 706 B |
| 4 | Compound Equal-only (byRecipientTime) | recipient == "recipient_050" AND sentAt == 50000 | O(log R + log T') | 71.8 µs | 1 937 B |
| 5 | In on byRecipient | recipient IN ["recipient_000", "recipient_001"] | O(k · log R) | 42.8 µs (k=2) / 1 720 µs (k=100) | 1 168 B (k=2) / 12 064 B (k=100) |
| 6 | In on bySentAt (RangeSummable) | sentAt IN [0, 1] | O(k · log T) | 81.2 µs (k=2) / 2 008 µs (k=100) | 1 756 B (k=2) / 9 784 B (k=100) |
| 7 | Range Query (AggregateSumOnRange) | sentAt > 50000 | O(log T) | 102.0 µs | 3 102 B |
| 8 | Compound == + Range (byRecipientTime) | recipient == "recipient_050" AND sentAt > 50000 | O(log R + log T') | 91.3 µs | 2 657 B |
| 9 | Carrier-Aggregate (In + range) | recipient IN [r000..r099] AND sentAt > 50000 (group_by = [recipient]) | O(k · log T') | 11 507.9 µs (k=100) | 169 064 B (k=100) |
Timing methodology: median of 5 iterations after one warmup, measured against the bench's 100 000-row fixture on a warmed rocksdb cache. The figures reflect the drive-layer execute_document_sum_request call (executor + grovedb proof generation, no network or tenderdash signature compose). Reproduce with cargo bench -p drive --bench document_sum_worst_case -- --test; grep µs from stderr.
Complexity variables. R = distinct recipients in the byRecipient merk-tree (= 100 in the fixture); T = distinct timestamps in the bySentAt merk-tree (= 100 000); T' = distinct timestamps per recipient in byRecipientTime's continuation (= 1 000 per recipient); k = number of values in the IN clause (2 here). Notably absent: the total document count N (100 000 here). Sum proofs read pre-committed sum_values from SumTree merk roots — they never enumerate the underlying documents, so proof generation cost is polylog(distinct index values), independent of N. Same big-O story as count.
The bySentAt index's T = 100 000 is unusually large (one distinct timestamp per row in this fixture); real tip jars would bucket timestamps coarsely. A reproducible-numbers fixture deliberately maximizes distinct values to stress the prove paths' merk-traversal cost — the per-merk-op count and proof-size numbers will skew accordingly when the bench publishes them.
Query 1 — Unfiltered Total Sum
select = SUM(amount)
where = (empty)
sum_property = "amount"
prove = true
Path query (primary-key SumTree fast path; no index walk needed):
path: ["@", contract_id, 0x01, "tip"]
query items: [Key(0x00)]
Verified element:
path: ["@", contract_id, 0x01, "tip"]
key: 0x00
element: SumTree { sum_value_or_default: 550000 }
Proof size: 580 bytes. Avg time: 23.6 µs. Verifier root hash: 95aa74708738c5254c706bbca3245b520022aad949674822218094bca15671b6 (same across every query in this chapter — same fixture state).
Proof display (GroveDBProof::Display):
Expand to see the structured proof (5 layers) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(KVValueHashFeatureTypeWithChildHash(0x00, Tree(0000000000010000fffffffffffeffff00000000000000000000000000000000), HASH[f25f49fc619abc59d984dcb322947509eb2a34f02217fc31dfc11423bee001e8], BasicMerkNode, HASH[d8c56d5b5d11c2e30a70694fac85259bb3169854e474ae8056ef91d5cb877000]))
1: Push(KVHash(HASH[a17138f666ae4ab19c1c2930ad94c7e29a6a82789398fc4d3a0b053d3499ac68]))
2: Parent
3: Push(Hash(HASH[143e80400b4fe5e0de4201bdf02853ac92347483a4a559040aa4ccb1ff4e3b03]))
4: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. The descent goes: top-level GroveDB root → @ (the DataContractDocuments root tree) → contract id → 0x01 (documents storage prefix) → tip doctype → finally the Key(0x00) payload at the bottom. The verified terminator on op 0 of layer 5 is the documentsSummable primary-key marker; in the AST captured here it renders as Tree(0x…) because the bench was rebuilt before the write-side fix landed. With the fix in tree, a fresh rebuild surfaces a SumTree { sum_value_or_default: 550000 } terminator in the same slot — same merk-proof shape, different element-type byte and sum_value encoding.
The descent stops at the doctype's primary-key tree — the green node at the top of the layout. Because documentsSummable: "amount" upgraded that tree to a SumTree, the total is one O(1) read with an O(log n) proof.
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[2f2b...]<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[5b1e...]<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[fba9...]<br/>value: Tree(tip)"]:::queried
L3_left["HASH[c53e...]<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>tip</b><br/>kv_hash=HASH[0fe5...]<br/>value: Tree(recipient/sentAt/0x00)"]:::queried
end
subgraph L5["Layer 5 — tip doctype merk-tree (TARGET layer)"]
direction TB
L5_target["<b>0x00</b><br/>kv_hash=HASH[f25f...]<br/>value: <b>SumTree sum=550000</b><br/>child_hash=HASH[d8c5...]<br/>(captured AST shows Tree+sum=0; fresh rebuild refreshes)"]:::target
L5_kv["KVHash[a171...]<br/>(opaque internal kv: sentAt or recipient)"]:::sibling
L5_right["HASH[143e...]<br/>(right subtree, opaque)"]:::sibling
L5_target --> L5_kv
L5_target --> L5_right
end
L1_root -. "value=Tree(merk_root[5b1e…])" .-> L2_q
L2_q -. "value=Tree(merk_root[fba9…])" .-> L3_q
L3_q -. "value=Tree(merk_root[0fe5…])" .-> L4_q
L4_q -. "value=Tree(merk_root[f25f…])" .-> L5_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;
Layers 1–4 are the standard contract-documents descent every query in this chapter shares; the divergence starts at Layer 5. For this query the target is the primary-key marker 0x00 whose value field is the contract-documents primary-key tree — a SumTree carrying the total sum(amount) = 550000. Each of the next four queries diverges at this layer to a different doctype child (recipient, sentAt, or recipient → recipient_050 → sentAt).
Query 2 — Equal on a Single Property (byRecipient)
select = SUM(amount)
where = recipient == "recipient_050"
sum_property = "amount"
prove = true
Path query:
path: ["@", contract_id, 0x01, "tip", "recipient"]
query items: [Key("recipient_050")]
Verified element:
path: ["@", contract_id, 0x01, "tip", "recipient"]
key: "recipient_050"
element: SumTree { sum_value_or_default: 1000 }
Proof size: 1 087 bytes. Avg time: 37.2 µs.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (6 layers) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVValueHash(recipient, Tree(000000000000003fffffffffffffffc000000000000000000000000000000000), HASH[f5801a1723ac6dfd5ff650eaa97d8b134255eef3ab8429dd516690dcfac74221]))
2: Parent
3: Push(Hash(HASH[143e80400b4fe5e0de4201bdf02853ac92347483a4a559040aa4ccb1ff4e3b03]))
4: Child)
lower_layers: {
recipient => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[b8804ea3f7998def339db7cadd6a6b29ba5bfadbdfa15b67802ef52e42adb68e]))
1: Push(KVHash(HASH[3056a7c2daf102d31d0f461d45e661303b1562311971debbf15f40938727a074]))
2: Parent
3: Push(Hash(HASH[c406363887b632d071f2ee6ed83df682aace9a5456997aa4f4b944576476fbb6]))
4: Push(KVHash(HASH[6b8ef1df5ba1299cd5b605dc7642be2ffb8356fd7f51c3a0498b6b657cddeebc]))
5: Parent
6: Push(Hash(HASH[347abc0b69e504bc619e9549e509c21be3c0ad1c9e03d8fb34bb5ed07e5cd26f]))
7: Push(KVHash(HASH[ff9b5006130777d589b77e6f5bdda0f86879daace181cdc8e3d97bc3718e0a48]))
8: Parent
9: Push(KVValueHashFeatureTypeWithChildHash(0x0000000000000032ffffffffffffffcd00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[264c6aac1a1acd864832a6f25ac42c626afb95dc79ac8c233d2b05c6df048f1c], BasicMerkNode, HASH[58680498d71fdda362f4a1815a0e0989b70a8cb287d8e40eaf84f8fe2cb48b6f]))
10: Child
11: Push(KVHash(HASH[5159e1ccad8c4ed1bbf7f52ec34e9ab4ee108559194e39b1af78cf42866b32cc]))
12: Parent
13: Push(Hash(HASH[4317777613ab1a0d6966670195ccce83dee1031fd37013c9ce625c016d1d6d17]))
14: Child
15: Push(KVHash(HASH[24f6646d8b75a6a428d05589c1cc1e80f1e52900924710ff6eafe9da0edc73d0]))
16: Parent
17: Push(Hash(HASH[14f8282bd0b5d9a8e7e471d3cfd215f02f5be2cfbf837c5e938c2871a2eddcb9]))
18: Child
19: Child
20: Child
21: Push(KVHash(HASH[cea6360efbf77b38d2ea206d508ea03c0d92fe7c02c5d9176aa322e8263a3acc]))
22: Parent
23: Push(Hash(HASH[209f3325816d5f02a1647311a4f1d9c68fcbacf1540be9b5ae65413f58ca3b26]))
24: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. The descent diverges from Q1 at layer 5: the tip doctype layer commits the recipient property-name subtree on op 1 (cyan-blue blue queried, valued Tree(0x6263...) — the merk root of byRecipient), then descends into layer 6, byRecipient itself. There the queried recipient_050 key (0x0000000000000032ffffffffffffffcd...) is reached as op 9 of a 25-op merk path, and its value is SumTree(73656e744174, 1000) — the 73656e744174 bytes are ASCII for "sentAt" (the byRecipientTime continuation pointer, NonCounted-wrapped at storage so it contributes 0 to the parent SumTree), and the 1000 is the per-recipient sum_value for recipient_050 (1 000 tips × amount = 1 = 1 000). Verified root hash 95aa7470…71b6.
The descent walks one extra layer into the recipient property-name subtree and stops at recipient_050. Because byRecipient is summable: "amount", that node is a SumTree carrying the per-recipient sum directly — no need to step into [0] to look at individual references, exactly the same shortcut count proofs take. (recipient_050's amount = (50 % 10) + 1 = 1, so the per-recipient sum is 1 000 × 1 = 1 000. A recipient with a different n % 10 lands a proportionally larger sum — see the fixture narrative above.)
Diagram: per-layer merk-tree structure
Layers 1–4 are byte-for-byte identical to Q1's diagram (root → @ → contract_id → 0x01). The descent diverges at Layer 5, where this query takes the recipient branch (rather than 0x00) and descends one extra grove layer to land on the verified target inside byRecipient.
flowchart TB
subgraph L5["Layer 5 — tip doctype merk-tree (proof view for `recipient`)"]
direction TB
L5_q["<b>recipient</b><br/>kv_hash=HASH[f580...]<br/>value: Tree (descent into byRecipient)"]:::queried
L5_left["HASH[15fd...]<br/>(left subtree, opaque)"]:::sibling
L5_right["HASH[143e...]<br/>(right subtree, opaque)"]:::sibling
L5_q --> L5_left
L5_q --> L5_right
end
subgraph L6["Layer 6 — byRecipient merk-tree (TARGET layer)"]
direction TB
L6_target["<b>recipient_050</b><br/>kv_hash=HASH[264c...]<br/>value: <b>SumTree sum=1000</b><br/>(value bytes 73656e744174 = `sentAt` continuation, NonCounted)<br/>child_hash=HASH[5868...]"]:::target
L6_boundary["Boundary commitments (24 merk ops):<br/>7 KVHash opaque sibling recipient kvs<br/>+ 6 Hash subtree commitments<br/>(prove recipient_050's position in byRecipient's<br/>binary merk tree of ~100 recipient entries)"]:::sibling
L6_target --> L6_boundary
end
L5_q -. "value=Tree(merk_root[byRecipient])" .-> 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 scale linearly with byRecipient's merk depth — they bind recipient_050 to its claimed position so the verifier can recompute byRecipient's merk root. The verified target itself is one KVValueHashFeatureTypeWithChildHash op whose SumTree(…, 1000) value carries the per-recipient sum directly; the continuation-pointer bytes 73656e744174 simply name the next layer's tree (which we never descend into for a Q2 point lookup).
Query 3 — Equal on a RangeSummable Property (bySentAt)
select = SUM(amount)
where = sentAt == 50000
sum_property = "amount"
prove = true
Path query:
path: ["@", contract_id, 0x01, "tip", "sentAt"]
query items: [Key(serialize_value_for_key("sentAt", 50000))]
Verified element:
path: ["@", contract_id, 0x01, "tip", "sentAt"]
key: serialize_value_for_key("sentAt", 50000)
element: SumTree { sum_value_or_default: 1 }
Proof size: 1 706 bytes — moderately larger than Query 2's 1 087 (a 619-byte delta) because the sentAt property-name subtree is a ProvableSumTree, so each merk node on the descent carries an extra i64 sum field versus byRecipient's plain NormalTree. (Same direction-and-magnitude delta the count chapter measures between byBrand and byColor.) Avg time: 72.1 µs (≈ 2× Query 2's 37.2 µs — the extra sum-field hash work on each layer dominates).
Proof display (GroveDBProof::Display):
Expand to see the structured proof (6 layers) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVHash(HASH[a17138f666ae4ab19c1c2930ad94c7e29a6a82789398fc4d3a0b053d3499ac68]))
2: Parent
3: Push(KVValueHash(sentAt, ProvableSumTree(800000000000ffff, 550000), HASH[3b3f5bf4e079c639895d84f8c5003fe135ccb46bf81b29fd3bdf415cddffe45c]))
4: Child)
lower_layers: {
sentAt => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[0a9e4f85b317569ed34bb8821fb5a7e4357e3a070413235d92ccfc09c4aae58f]))
1: Push(KVHashSum(HASH[be99a0622f1400aaa04dc460566babac9c1a4955b3eeb1c5780dfd46ec716222], 360430))
2: Parent
3: Push(Hash(HASH[e8c7c1c4fbe14e2d5cc9e460788baad347ea50eed38e3bf0316288d3aa38c2d4]))
4: Push(KVHashSum(HASH[053e0adbc34321342c734571ab822b3c9e2fd5aa2efc267f4e09abd69670dfad], 180214))
5: Parent
6: Push(Hash(HASH[b49dc66868027c23f0e5bfde974330d67f077636fa2f1a6c69d96a7db380e4f2]))
7: Push(KVHashSum(HASH[86cb3966ee86191ce18f754b3c8492f9ad1929c9e540da193cf63899fd311d3b], 5622))
8: Parent
9: Push(Hash(HASH[41bf237b6f834b4271e191423907567fd3144d69bf6c75fd8d475807928063ee]))
10: Push(KVHashSum(HASH[f1600f73cf84dc6ae4de0057887f1fe60a373073863ee352ebede717795b1c91], 2810))
11: Parent
12: Push(Hash(HASH[0765b948c17ac92fc10c83b4480283371bcd9c3a4745e62242d842f2f9125fe4]))
13: Push(KVHashSum(HASH[a1d9da3b90a69411dcbba3934f3dd86daeeec542373c261ad36ddb072f0c61b8], 688))
14: Parent
15: Push(Hash(HASH[60ae1d862ec2ba4f63e041741bb93a539882e9620302cfc343ac5d0f339d8e21]))
16: Push(KVHashSum(HASH[133fc6dec68084db2d8c5273011a000568f49447bbeeb1c4500776a42c6de434], 170))
17: Parent
18: Push(KVValueHashFeatureTypeWithChildHash(0x800000000000c350, SumTree(00, 1), HASH[b22d3790d948924dc6311518f2872b53c0590d3cc497d7a653f1ce7b9923e499], ProvableSummedMerkNode(1), HASH[f3c390a0a62080d8a85c55e5c08c01546f0086ae2456fb7f23cf88d7d3d0c44b]))
19: Push(KVHashSum(HASH[4fa59ee6c08136a261a9b4dca592f5df25063b8f190543a9556946da3bf1d4d7], 6))
20: Parent
21: Push(Hash(HASH[6191f6b8bcfac3d6772193061c3b17dbc218a80657b018d8e9455ec571922d71]))
22: Child
23: Push(KVHashSum(HASH[5d412517c8ad8e784679852d2da663cf231d59f6e9f24bee36f26e9e6a1be900], 28))
24: Parent
25: Push(Hash(HASH[7af88dd3f234aa1e3bbf69e4543b63cd2920c16c4a04c7e2bbfef23405b0181c]))
26: Child
27: Push(KVHashSum(HASH[f412f555a36be8dc7b71d933ef60f51a34dcb3995aacf9ca82e6e5e31f2c0601], 70))
28: Parent
29: Push(Hash(HASH[7ece6d26bbaa79713cc72c05be520d699f98e6ffb66187d3d1a98f0de85db1dc]))
30: Child
31: Child
32: Push(KVHashSum(HASH[758bd7f1ade8550bb59935448441bd81421f5c8b1d77da46581f576a326c791f], 348))
33: Parent
34: Push(Hash(HASH[b2b507154c77192abad2aebd28953ab177560b4c1d515c0630821dd0e6d22b67]))
35: Child
36: Child
37: Push(KVHashSum(HASH[f2e039a1921a0d514ca714e7501d00268649420c96036fa4d6f803cb2838cdb7], 1390))
38: Parent
39: Push(Hash(HASH[145cbb78cc936b222cc9e6b06ae684aa47a1bf5479acdb35c92cbc57dc985a37]))
40: Child
41: Child
42: Child
43: Push(KVHashSum(HASH[36cf0712fbcca1239ce0cffb8b262b0a8b06a072015826c7f2a56d2bc7611169], 11262))
44: Parent
45: Push(Hash(HASH[5120fee7ab2fa02978f3dd43b6101dbc9b22de0d45c9e65976f5562a98ba4607]))
46: Child
47: Push(KVHashSum(HASH[ae76541450c5efae715f617db2d63a401da7bc2decaac3720127af252e99fa0d], 22520))
48: Parent
49: Push(Hash(HASH[5758f27a188b7d9a0b8c488374f80031671c662eb12828565146a6117344d5ea]))
50: Child
51: Push(KVHashSum(HASH[c60627bbfc24d9c01e6e7e6d8e79743884916f04904d08ca1d56e17ca8c52989], 45048))
52: Parent
53: Push(Hash(HASH[c7be4432e5634e8c8f2d0a2074b2f72a8f0fea8859a8b5f1bd6dd6556d16bf7e]))
54: Child
55: Push(KVHashSum(HASH[9f79592b39f89b6f6fed13d0fcd2e548d57735169a9e583960678dde3a5f0a05], 90102))
56: Parent
57: Push(Hash(HASH[cc7d10c23ef37f717891e6390c1fa52bf8133b230df6871c8cf85e1fcec2e9b2]))
58: Child
59: Child
60: Child
61: Push(KVHashSum(HASH[ca275ed3d5729250a4a67e2cc90fac909086fac99e386fc90688df2bb01b3eaa], 550000))
62: Parent
63: Push(Hash(HASH[8593bd2bc903da34da11e15f9a649cec37b39487ad59375020adef300a8b7482]))
64: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. Same first four layers as Q1/Q2; at layer 5 (the tip doctype) the descent takes the sentAt branch on op 3 (its value is ProvableSumTree(800000000000ffff, 550000) — a ProvableSumTree whose root-committed sum is 550 000, the full timeline sum) and descends into layer 6, bySentAt. Layer 6 is structurally different from Q2's byRecipient layer: every internal kv op is a KVHashSum (a hash with its subtree's i64 sum), and every opaque subtree commit on the merk-boundary walk carries its own running sum (e.g. op 61's KVHashSum(…, 550000) is the full-tree root-commitment). The terminator on op 18 is KVValueHashFeatureTypeWithChildHash(0x800000000000c350, SumTree(00, 1), …, ProvableSummedMerkNode(1), …) — the ProvableSummedMerkNode(1) feature-type marks it as living inside a ProvableSumTree with its own contributing sum of 1, and SumTree(00, 1) is the per-timestamp value tree (sum_value_or_default = 1, since sentAt = 50 000 was assigned amount = (50000 % 10) + 1 = 1). Verified root hash 95aa7470…71b6.
Diagram: per-layer merk-tree structure
Layers 1–4 are byte-for-byte identical to Q1's. The descent diverges at layer 5 onto sentAt, then layer 6 walks the bySentAt ProvableSumTree — yellow rather than gray for sibling commits because every internal kv carries a sum field.
flowchart TB
subgraph L5["Layer 5 — tip doctype merk-tree (proof view for `sentAt`)"]
direction TB
L5_q["<b>sentAt</b><br/>kv_hash=HASH[3b3f...]<br/>value: <b>ProvableSumTree</b> (root sum=550000)<br/>(descent into bySentAt)"]:::queried
L5_left["HASH[15fd...]<br/>(left subtree, opaque)"]:::sibling
L5_kv["KVHash[a171...]<br/>(opaque internal kv: 0x00 or recipient)"]:::sibling
L5_q --> L5_left
L5_q --> L5_kv
end
subgraph L6["Layer 6 — bySentAt ProvableSumTree merk-tree (TARGET layer)"]
direction TB
L6_target["<b>sentAt=50000 (0x800000000000c350)</b><br/>kv_hash=HASH[b22d...]<br/>value: <b>SumTree(00, sum=1)</b><br/>feature: ProvableSummedMerkNode(1)<br/>child_hash=HASH[f3c3...]"]:::target
L6_pst["Boundary commitments (64 merk ops):<br/>every internal kv is a KVHashSum (hash + subtree sum)<br/>every opaque sibling is a Hash subtree commitment<br/>(both contribute to the per-node sum aggregation<br/>that makes AggregateSumOnRange a single-pass primitive)"]:::pst
L6_target --> L6_pst
end
L5_q -. "value=ProvableSumTree(merk_root[bySentAt], sum=550000)" .-> L6_target
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef pst fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The yellow pst node summarizes the 64-op ProvableSumTree merk descent: every kv-and-hash sibling carries an i64 sum field so the verifier can recompute the parent's sum, not just its hash. That extra hash work is what makes Q3 a ~2× longer wall-clock proof than Q2 even though the descent depth is identical.
Query 4 — Compound Equal-only (byRecipientTime)
select = SUM(amount)
where = recipient == "recipient_050" AND sentAt == 50000
sum_property = "amount"
prove = true
Path query:
path: ["@", contract_id, 0x01, "tip", "recipient", "recipient_050", "sentAt"]
query items: [Key(serialize_value_for_key("sentAt", 50000))]
Verified element:
path: ["@", contract_id, 0x01, "tip", "recipient", "recipient_050", "sentAt"]
key: serialize_value_for_key("sentAt", 50000)
element: SumTree { sum_value_or_default: 0 } (no tip at this recipient×sentAt pair)
Proof size: 1 937 bytes. Avg time: 71.8 µs.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (8 layers: 6 path layers + 2 byRecipientTime continuation layers) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVValueHash(recipient, Tree(000000000000003fffffffffffffffc000000000000000000000000000000000), HASH[f5801a1723ac6dfd5ff650eaa97d8b134255eef3ab8429dd516690dcfac74221]))
2: Parent
3: Push(Hash(HASH[143e80400b4fe5e0de4201bdf02853ac92347483a4a559040aa4ccb1ff4e3b03]))
4: Child)
lower_layers: {
recipient => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[b8804ea3f7998def339db7cadd6a6b29ba5bfadbdfa15b67802ef52e42adb68e]))
1: Push(KVHash(HASH[3056a7c2daf102d31d0f461d45e661303b1562311971debbf15f40938727a074]))
2: Parent
3: Push(Hash(HASH[c406363887b632d071f2ee6ed83df682aace9a5456997aa4f4b944576476fbb6]))
4: Push(KVHash(HASH[6b8ef1df5ba1299cd5b605dc7642be2ffb8356fd7f51c3a0498b6b657cddeebc]))
5: Parent
6: Push(Hash(HASH[347abc0b69e504bc619e9549e509c21be3c0ad1c9e03d8fb34bb5ed07e5cd26f]))
7: Push(KVHash(HASH[ff9b5006130777d589b77e6f5bdda0f86879daace181cdc8e3d97bc3718e0a48]))
8: Parent
9: Push(KVValueHash(0x0000000000000032ffffffffffffffcd00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[264c6aac1a1acd864832a6f25ac42c626afb95dc79ac8c233d2b05c6df048f1c]))
10: Child
11: Push(KVHash(HASH[5159e1ccad8c4ed1bbf7f52ec34e9ab4ee108559194e39b1af78cf42866b32cc]))
12: Parent
13: Push(Hash(HASH[4317777613ab1a0d6966670195ccce83dee1031fd37013c9ce625c016d1d6d17]))
14: Child
15: Push(KVHash(HASH[24f6646d8b75a6a428d05589c1cc1e80f1e52900924710ff6eafe9da0edc73d0]))
16: Parent
17: Push(Hash(HASH[14f8282bd0b5d9a8e7e471d3cfd215f02f5be2cfbf837c5e938c2871a2eddcb9]))
18: Child
19: Child
20: Child
21: Push(KVHash(HASH[cea6360efbf77b38d2ea206d508ea03c0d92fe7c02c5d9176aa322e8263a3acc]))
22: Parent
23: Push(Hash(HASH[209f3325816d5f02a1647311a4f1d9c68fcbacf1540be9b5ae65413f58ca3b26]))
24: Child)
lower_layers: {
0x0000000000000032ffffffffffffffcd00000000000000000000000000000000 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[ad21cf215639bca21e163333196de5b1774e0e91bf266a323c3a0b78c8cf7998]))
1: Push(KVValueHash(sentAt, NotSummed(ProvableSumTree(800000000000c7ce, 1000)), HASH[fbe3df47d0e3ee0dbdb9a1491fc05d93ae26f8cb914fb240a42e9989a3752cbc]))
2: Parent)
lower_layers: {
sentAt => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[585d1074d4c73df4e60784323fb6e1cd9a45a7f6fed92ca43ac78a1a575efa16]))
1: Push(KVHashSum(HASH[471d8a8c670fb4bc4c30d251a7fb398f5946a076cfd3c734ce7c131a6a922ae7], 511))
2: Parent
3: Push(Hash(HASH[39bb4483b5c84d178863fb016add610ae44876b0450a9c2f75de97fcdd60c8b5]))
4: Push(KVHashSum(HASH[59629e7456a1ef7da12d0d02c0c3501f68e27f92aa737f088b34a05ddebb42e7], 255))
5: Parent
6: Push(Hash(HASH[99363add734e45862d731777f87ca7a018109aa62aa489e3c5a5515dc965678b]))
7: Push(KVHashSum(HASH[542ff7575944b1293665b9a4ea5a5a49945e2d3206280d9cbded80afe404a7de], 127))
8: Parent
9: Push(Hash(HASH[d1b19a359a223351d14e5f21b8fa3e49a012e0b4bb6237c5d73b08bdd9b4d9e8]))
10: Push(KVHashSum(HASH[79ab80617285543900f09137ef8cc42c3f715a6be449c7453d6babceb602c36d], 63))
11: Parent
12: Push(Hash(HASH[497640ee5a5972a73c9484d129a9df3dc17c0e5f06a631d26e824f817706639a]))
13: Push(KVHashSum(HASH[45ee1ce73fc4156e923393f2b7113f22ae3e10b9e74578c46e263c2fb8623af2], 31))
14: Parent
15: Push(Hash(HASH[97daec9f53ac09843034f165faf5410e54882561f3ee345281cf51f0972e29cc]))
16: Push(KVDigestSum(0x800000000000c31e, HASH[110023b15d77b39d67044847913b994adf94cdb8ecfa5bce2abd4d37d6ac68c7], 7))
17: Parent
18: Push(KVDigestSum(0x800000000000c382, HASH[1ea127fb3003e8de7bcfbe341b29da4cff8b8c119dfa10d47eaca70fb7a62d54], 1))
19: Push(KVHashSum(HASH[177c272d0a693d73e8543f41adbce6d964874573d9eacb6fa7bb8d8d5eefc4a3], 3))
20: Parent
21: Push(Hash(HASH[aad686ea6ee57db81ad554934281a5b4c383efd1dd2142011b11f2bf73872d7f]))
22: Child
23: Child
24: Push(KVHashSum(HASH[d0c8fd2ef0a5ed9e51c335d6a7967e16bc0eb02c6a4fc8e7e1e86ec599717ede], 15))
25: Parent
26: Push(Hash(HASH[d13f24f322e026b7909002ca6b79b76c71aad9117601366b9bc867d649cb885b]))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVHashSum(HASH[f94972b44e022eca9d291dda5a150bf75a9ab5cf759d4211bf5c8643ffbe7585], 1000))
34: Parent
35: Push(Hash(HASH[de4170da8225c95a1ffc8dbeb08e2a92add3ea7ac2b7972eb786bfdb1bee4207]))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. Q4 is the deepest single-key sum proof in the chapter: 8 LayerProofs because the byRecipientTime descent walks recipient → recipient_050 → sentAt → terminator. Layers 1–5 mirror Q2's descent verbatim (root → @ → contract → 0x01 → tip → recipient). Layer 6 lands on recipient_050 whose value is now plain SumTree(73656e744174, 1000) (no FeatureType wrapper because this proof descends into it rather than terminating there). Layer 7 is recipient_050's continuation merk-tree, a single-key tree whose only entry is the sentAt property name pointing at a NotSummed(ProvableSumTree(…, 1000)) — the NotSummed wrapper is the storage-layer signal that this continuation's sum doesn't propagate up to recipient_050's own SumTree (the parent's 1 000 already aggregates the value-tree references; this continuation is just a sibling index, contributing 0). Layer 8 walks the byRecipientTime ProvableSumTree (37 ops, the same per-node sum-bearing shape as Q3's bySentAt) but does NOT terminate at 0x800000000000c350: the queried sentAt-50000 key is absent under recipient_050 (since recipient_050 owns rows 50, 150, …, 99950, none of which are sentAt = 50000). The proof commits a complete merk path with no terminator op, which verify_query reports as an empty results vec (the verifier-side absence proof). Verified root hash 95aa7470…71b6.
Two property-name descents (recipient, then under recipient_050 the byRecipientTime continuation's sentAt). For this specific filter no row lands at recipient_050 ∧ sentAt = 50000 (recipient_050 owns rows {50, 150, …, 99 950}; sentAt = 50000 is owned by recipient_000), so the verified element is a SumTree with sum_value_or_default = 0 proving absence. The proof is still 1 937 bytes — absence proofs walk the same depth as present-key proofs, just with a different terminator merk node.
Diagram: per-layer merk-tree structure
Layers 1–5 are identical to Q2's; Q4's signature divergence happens at layers 6–8 where the byRecipientTime compound index threads through recipient_050's NotSummed(ProvableSumTree) continuation.
flowchart TB
subgraph L6["Layer 6 — byRecipient merk-tree (mid-descent, NOT a target)"]
direction TB
L6_q["<b>recipient_050</b><br/>kv_hash=HASH[264c...]<br/>value: SumTree(73656e744174, sum=1000)<br/>(descends one more layer)"]:::queried
L6_boundary["Boundary commitments (24 ops):<br/>same byRecipient path Q2 walks, just<br/>without the FeatureTypeWithChildHash wrapper"]:::sibling
L6_q --> L6_boundary
end
subgraph L7["Layer 7 — recipient_050 continuation merk-tree (single key)"]
direction TB
L7_q["<b>sentAt</b><br/>kv_hash=HASH[fbe3...]<br/>value: <b>NotSummed(ProvableSumTree(…, 1000))</b><br/>(NotSummed = continuation, contributes 0 to L6's sum)"]:::queried
L7_sib["HASH[ad21...]<br/>(left subtree, opaque)"]:::sibling
L7_q --> L7_sib
end
subgraph L8["Layer 8 — byRecipientTime sentAt ProvableSumTree (TARGET layer, absent key)"]
direction TB
L8_absent["sentAt=50000 (queried key) — <b>absent</b><br/>(no terminator op committed; recipient_050<br/>owns rows 50, 150, …, 99950 only)"]:::target
L8_neighbors["Boundary commitments (37 ops):<br/>KVDigestSum siblings at 0x800000000000c31e (sum=7) and<br/>0x800000000000c382 (sum=1) bracket the absent key<br/>+ 17 KVHashSum / Hash subtree commits across the<br/>ProvableSumTree's per-node sum-bearing structure"]:::pst
L8_absent --> L8_neighbors
end
L6_q -. "value=SumTree(merk_root[r050-continuation])" .-> L7_q
L7_q -. "value=NotSummed(ProvableSumTree(merk_root[byRT.sentAt]))" .-> L8_absent
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef pst fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The absent-key shape is structurally important: the bracketing siblings 0x800000000000c31e and 0x800000000000c382 (op 16, op 18 in layer 8) are what convince the verifier the queried 0x800000000000c350 doesn't exist between them. The proof commits enough of the merk-tree to recompute the parent root with the gap intact — a non-membership witness in the same proof shape as a membership witness.
Query 5 — In on byRecipient
select = SUM(amount)
where = recipient IN ["recipient_000", "recipient_001"]
sum_property = "amount"
prove = true
Path query (per-In-value point-lookup fan-out):
path: ["@", contract_id, 0x01, "tip", "recipient"]
query items: [Key("recipient_000"), Key("recipient_001")]
Verified entries (two SumEntrys under the entries variant):
SumEntry { in_key: None, key: "recipient_000", sum: 1000 } (amount=1)
SumEntry { in_key: None, key: "recipient_001", sum: 2000 } (amount=2)
Proof size: 1 168 bytes for k=2. The bench's report_proof_sizes measures the same shape at k=100 across all distinct recipients — 12 064 bytes, scaling roughly linearly with |In values| because each branch's merk descent is independent. The per-branch marginal cost is ≈ 109 bytes (10 880 / 99 marginal branches). Avg time: 42.8 µs (k=2) / 1 720 µs (k=100) — roughly 17 µs of marginal time per added In value, consistent with the per-branch merk-descent cost on byRecipient.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (6 layers, dual-target merk-path at the bottom) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVValueHash(recipient, Tree(000000000000003fffffffffffffffc000000000000000000000000000000000), HASH[f5801a1723ac6dfd5ff650eaa97d8b134255eef3ab8429dd516690dcfac74221]))
2: Parent
3: Push(Hash(HASH[143e80400b4fe5e0de4201bdf02853ac92347483a4a559040aa4ccb1ff4e3b03]))
4: Child)
lower_layers: {
recipient => {
LayerProof {
proof: Merk(
0: Push(KVValueHashFeatureTypeWithChildHash(0x0000000000000000ffffffffffffffff00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[2c932396123fe6bae5fa3e4fa42225852e08a2dcf7600991e79fb9347de7506e], BasicMerkNode, HASH[307798135a4d282b0306f8f0a652c99391fb89a193b1f219632c956b31fb1351]))
1: Push(KVValueHashFeatureTypeWithChildHash(0x0000000000000001fffffffffffffffe00000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[43c65eed82b8e5d08b7fea673bf120a82332adbffb8582e0f8a3205190fab720], BasicMerkNode, HASH[8723320b1000d7ca62f0057154cc9ce7e79b31ceeec9e5ea16db382efdf6a81f]))
2: Parent
3: Push(Hash(HASH[e0442e4426d554662f91691fba1bec4c0eddac54662493b3964b08538fea33fe]))
4: Child
5: Push(KVHash(HASH[2f5f739085124d63217a3b5c8276da943d0901c1fc482010e5a50c2f73b36549]))
6: Parent
7: Push(Hash(HASH[93e907e7310d06719783bd32af4653a620b3d2f46c9b54bf1d5edb45928866f7]))
8: Child
9: Push(KVHash(HASH[ca38e3d00da85a2b31477b69b9f71e0c5535a1d9c1bca5b54c875444f7a0bad9]))
10: Parent
11: Push(Hash(HASH[274c7bf51d753c5ed1cc44997d2ed956e30c6574609c165b073a2c6f75e9af58]))
12: Child
13: Push(KVHash(HASH[db46825673ab3057db34992612299ceeca04f94152ac048f647cc92afbd1d771]))
14: Parent
15: Push(Hash(HASH[af91e1e902af6a64f73a4f4abd49ccb5893029568e23ec2e73452dd1fb58940a]))
16: Child
17: Push(KVHash(HASH[3056a7c2daf102d31d0f461d45e661303b1562311971debbf15f40938727a074]))
18: Parent
19: Push(Hash(HASH[22b84dcbedf3b829ad645fd68fa5eb1c59dadb2ab3cd098efd998ca247902c95]))
20: Child
21: Push(KVHash(HASH[cea6360efbf77b38d2ea206d508ea03c0d92fe7c02c5d9176aa322e8263a3acc]))
22: Parent
23: Push(Hash(HASH[209f3325816d5f02a1647311a4f1d9c68fcbacf1540be9b5ae65413f58ca3b26]))
24: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. Same 5-layer descent as Q2 (root → @ → contract → 0x01 → tip → recipient); the divergence is at the bottom layer where the proof reveals two KVValueHashFeatureTypeWithChildHash terminator ops back-to-back (op 0 for recipient_000, op 1 for recipient_001), each carrying its own SumTree(73656e744174, …) with per-recipient sum (1000 and 2000 respectively). The 24-op boundary walk that surrounds them is the same shape as Q2's — proving the two queried keys' positions inside byRecipient's ~100-entry merk-tree by committing the bracketing opaque siblings. Verified entries via verify_query are two (path, key, SumTree { sum_value_or_default: … }) rows, one per In branch. Verified root hash 95aa7470…71b6.
The entries variant carries in_key: None because the In is itself the terminator (no compound-prefix prefix). Compare with Query 6, which has the same property-and-position In but on a rangeSummable index.
Diagram: per-layer merk-tree structure
Layers 1–5 mirror Q2's prefix exactly. The divergence is at layer 6: where Q2 had one cyan target, Q5 has two — and the boundary commits between them are shared by both descents.
flowchart TB
subgraph L6["Layer 6 — byRecipient merk-tree (DUAL TARGET layer)"]
direction TB
L6_t0["<b>recipient_000</b><br/>kv_hash=HASH[2c93...]<br/>value: <b>SumTree sum=1000</b><br/>child_hash=HASH[3077...]"]:::target
L6_t1["<b>recipient_001</b><br/>kv_hash=HASH[43c6...]<br/>value: <b>SumTree sum=2000</b><br/>child_hash=HASH[8723...]"]:::target
L6_boundary["Boundary commitments (~22 ops shared across<br/>both descents):<br/>6 KVHash internal siblings + 6 Hash subtree commits<br/>(prove recipient_000/recipient_001 are adjacent<br/>in byRecipient's binary merk tree)"]:::sibling
L6_t0 --> L6_boundary
L6_t1 --> L6_boundary
end
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
Per-branch byte cost is sub-linear because the two targets share most of the merk-boundary walk (only their kv-hashes themselves are per-branch; siblings amortize). At k=100 with all 100 recipients covered by 2 In branches each, every byRecipient entry becomes a target and only the merk root's two-level boundary stays opaque — the most efficient byte-per-key shape an In on byRecipient can hit.
Query 6 — In on bySentAt (RangeSummable)
select = SUM(amount)
where = sentAt IN [0, 1]
sum_property = "amount"
prove = true
Path query:
path: ["@", contract_id, 0x01, "tip", "sentAt"]
query items: [Key(serialize_value_for_key("sentAt", 0)),
Key(serialize_value_for_key("sentAt", 1))]
Verified entries:
SumEntry { in_key: None, key: serialize_value_for_key("sentAt", 0), sum: 1 }
SumEntry { in_key: None, key: serialize_value_for_key("sentAt", 1), sum: 2 }
(Per the bench's amount = (row % 10) + 1 schedule, row 0 has amount = 1 and row 1 has amount = 2.)
Proof size: 1 756 bytes for k=2; 9 784 bytes at k=100. Per-branch marginal cost ≈ 81 bytes — somewhat less than Query 5's 109 bytes per branch, because the bySentAt subtree's distinct keys are dense integers (one per row) and share more merk-path prefix than byRecipient's hash-derived keys. Avg time: 81.2 µs (k=2) / 2 008 µs (k=100) — slightly higher per-branch time than Q5 despite smaller per-branch bytes; the ProvableSumTree's per-node sum-field hash work eats most of the prefix-sharing savings.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (6 layers, dual-target in a ProvableSumTree) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVHash(HASH[a17138f666ae4ab19c1c2930ad94c7e29a6a82789398fc4d3a0b053d3499ac68]))
2: Parent
3: Push(KVValueHash(sentAt, ProvableSumTree(800000000000ffff, 550000), HASH[3b3f5bf4e079c639895d84f8c5003fe135ccb46bf81b29fd3bdf415cddffe45c]))
4: Child)
lower_layers: {
sentAt => {
LayerProof {
proof: Merk(
0: Push(KVValueHashFeatureTypeWithChildHash(0x8000000000000000, SumTree(00, 1), HASH[7196506382214367916d1e1e0cff254b14f89815f007840414956a077dd53148], ProvableSummedMerkNode(1), HASH[ef96a09b8f07fdbb7d17c3e86a7bdc954aae31c8e2a51900ed2b7d92a6ab9953]))
1: Push(KVValueHashFeatureTypeWithChildHash(0x8000000000000001, SumTree(00, 2), HASH[6b4e9f17f2121cb658ec356b970a91b5ad79032a57e304a3507f63d813f2da8d], ProvableSummedMerkNode(6), HASH[8abb1cbf9ae45b42ff519e6f187bd4fbadee93ab64afef59089aaa94ed46b502]))
2: Parent
3: Push(Hash(HASH[e03fc8c98c17d3574f4fe6a822916775844555dd63276f2e7289f11ec5aa043d]))
4: Child
5: Push(KVHashSum(HASH[4eb5727c0eb7018148785596abb4767ae93002c4ab76b4f565d31b5235b55a39], 28))
6: Parent
7: Push(Hash(HASH[843e47cd7806eb5309f780a529fe26b245b22cedd46ee3ea62ac23ab6767d59b]))
8: Child
9: Push(KVHashSum(HASH[af802589532730912220f760f96036c67bb64c60dc4cdc468d1c978cb78972fe], 70))
10: Parent
11: Push(Hash(HASH[9adc74e0427bccca23c5adbd648610c340edec52566da65d51f42bcfb1f78f93]))
12: Child
13: Push(KVHashSum(HASH[deb2eb99281aa70288d4accac22faba3681fcdd0258bd5c6d06a2b7ee6cb6624], 166))
14: Parent
15: Push(Hash(HASH[7cd9232374df6e9e98d64e199876edcc91d1016b6541acb867016bcdea24b7d3]))
16: Child
17: Push(KVHashSum(HASH[2acdf1c54d23ebf0874620d776ec72326bc88300a1fd724fbc24ed8c2d921b32], 336))
18: Parent
19: Push(Hash(HASH[2f62bfca826600367f5802e4ff86ac652a225e87dbddcea3d7b38fc922d18b59]))
20: Child
21: Push(KVHashSum(HASH[45f178d25bec1d9b81115e2f023db10ea0117d3c508409d61c2390a9244484e1], 688))
22: Parent
23: Push(Hash(HASH[f983a5b23027388a35323554689f394267399ad40071c6feaeeebd415d5e22d7]))
24: Child
25: Push(KVHashSum(HASH[fe3e84de3d4be2e935b0408a0ef427e277292d7675e88fabf0b9a71346f1ad7d], 1390))
26: Parent
27: Push(Hash(HASH[4552635b13dd23588d3fcdf3382a0a7fe0600f73d118f28b27507bbd7113842b]))
28: Child
29: Push(KVHashSum(HASH[d49e4a23ef475529c6b7116795a0f587d3dd191a6c51b42641e277623afa03e5], 2806))
30: Parent
31: Push(Hash(HASH[27cd36370fc15a994480197224b944ba24f775dcb6b34f28b74f25c0dd0582b9]))
32: Child
33: Push(KVHashSum(HASH[5e4ce1f7f36aca9f81c33b45d945c84488fdf1915402b07914cbbd0f35917af1], 5616))
34: Parent
35: Push(Hash(HASH[1ccfd53d7a30abdd7171a5a0ce63d22c7266ed0879e8353463fefb271112ba13]))
36: Child
37: Push(KVHashSum(HASH[26794659891cef5a5e0edf7947bbca061307b947783887b25ea3323277211af3], 11248))
38: Parent
39: Push(Hash(HASH[1a7593e8e1d66a041bf127649256d0b836368c3f41ec58fa714fbaceb6b5e9fb]))
40: Child
41: Push(KVHashSum(HASH[c42b2154308418eabefedc2b828dbc599009fbb2b0965f057480d8f9cf1e096d], 22510))
42: Parent
43: Push(Hash(HASH[3b81157cc8121f546f746342f60a4d6b325b965874342c094490f1d21f0cdef3]))
44: Child
45: Push(KVHashSum(HASH[2d2bd624f09591fdecf3068dbe47314ff108f3cb4f99482ebd0abbb9c947e257], 45046))
46: Parent
47: Push(Hash(HASH[71958692ac25e70f1d1df3fb5b85c2b90eac37ae745be90b931992b2f9dbfa94]))
48: Child
49: Push(KVHashSum(HASH[150a4c0a44464b24dbde8a45106e370fc189b31676bfd6284763923381a6d434], 90096))
50: Parent
51: Push(Hash(HASH[5e468c0f9805ee381d4c59dd961adc8dee1ab003834aa17fadd9f51411804147]))
52: Child
53: Push(KVHashSum(HASH[a133f55e1f33e2b3a2a78f9f89c3aff8fd06f0ad25254490a42beef2c130e9dd], 180208))
54: Parent
55: Push(Hash(HASH[620ccd98c998cb3a503ee12098a6617743a99ffeb644069927ce7ebd8b70f76a]))
56: Child
57: Push(KVHashSum(HASH[be99a0622f1400aaa04dc460566babac9c1a4955b3eeb1c5780dfd46ec716222], 360430))
58: Parent
59: Push(Hash(HASH[f404bfecb3e178393455544d654039f0cb77c0e0d918cc00d85d22d2707cbf0d]))
60: Child
61: Push(KVHashSum(HASH[ca275ed3d5729250a4a67e2cc90fac909086fac99e386fc90688df2bb01b3eaa], 550000))
62: Parent
63: Push(Hash(HASH[8593bd2bc903da34da11e15f9a649cec37b39487ad59375020adef300a8b7482]))
64: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. Same first 5 layers as Q3 (the descent to bySentAt). The bottom layer reveals both queried timestamps as adjacent KVValueHashFeatureTypeWithChildHash terminator ops: op 0 for sentAt=0 (SumTree(00, 1), amount = 1) and op 1 for sentAt=1 (SumTree(00, 2), amount = 2). Each terminator's feature-type marker is ProvableSummedMerkNode(n) with n matching the per-node committed sum — this is the per-node-sum machinery that makes the surrounding bySentAt tree a ProvableSumTree. The 62-op boundary walk surrounds them with the same per-node sum-bearing structure as Q3, except now two leaves are revealed instead of one. Verified root hash 95aa7470…71b6.
Same structural shape as Query 5 — the difference is the property-name subtree is a ProvableSumTree rather than a NormalTree, so each descent step on the In branches carries a sum field. For point lookups that's pure overhead (we don't use the per-node sums); the payoff lands on Query 7.
Diagram: per-layer merk-tree structure
Layers 1–5 mirror Q3's; the difference is at layer 6 where two adjacent leaves are revealed inside the ProvableSumTree.
flowchart TB
subgraph L6["Layer 6 — bySentAt ProvableSumTree merk-tree (DUAL TARGET layer)"]
direction TB
L6_t0["<b>sentAt=0 (0x8000000000000000)</b><br/>kv_hash=HASH[7196...]<br/>value: <b>SumTree(00, sum=1)</b><br/>feature: ProvableSummedMerkNode(1)<br/>child_hash=HASH[ef96...]"]:::target
L6_t1["<b>sentAt=1 (0x8000000000000001)</b><br/>kv_hash=HASH[6b4e...]<br/>value: <b>SumTree(00, sum=2)</b><br/>feature: ProvableSummedMerkNode(6)<br/>child_hash=HASH[8abb...]"]:::target
L6_pst["Boundary commitments (62 merk ops):<br/>every internal kv is a KVHashSum (hash + sum)<br/>every opaque sibling is a Hash subtree commitment<br/>(running sums on each boundary node:<br/>28 → 70 → 166 → 336 → 688 → 1390 → 2806 → 5616 → …<br/>→ 550000 at the merk root, the full timeline sum)"]:::pst
L6_t0 --> L6_pst
L6_t1 --> L6_pst
end
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef pst fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The per-node sum chain 28 → 70 → 166 → … → 550000 is what makes this also a sum-bearing path: a verifier who walked the same descent for an AggregateSumOnRange instead would extract a running sum from the same KVHashSum ops rather than reading individual leaves. That's how Q3 / Q6 (point lookups on a ProvableSumTree) and Q7 (range collapse on the same tree) share infrastructure and differ only in which nodes the proof reveals as terminators.
Query 7 — Range Query (AggregateSumOnRange)
select = SUM(amount)
where = sentAt > 50000
sum_property = "amount"
prove = true
Path query:
path: ["@", contract_id, 0x01, "tip", "sentAt"]
query items: AggregateSumOnRange(RangeAfter(serialize_value_for_key("sentAt", 50000)..))
Verified result (returned by GroveDb::verify_aggregate_sum_query):
(root_hash, sum) where sum = 274 999
49 999 rows have sentAt > 50 000 (the half-open (50000, ∞) range excludes sentAt = 50000 itself). Per the fixture's amount = (row % 10) + 1 schedule, rows 50 001..99 999 cycle through [2, 3, 4, 5, 6, 7, 8, 9, 10, 1] for 4 999 full cycles + a 9-element tail. The sum works out to 4 999 × 55 + (2 + 3 + … + 10) = 274 945 + 54 = 274 999, which matches the verified value byte-for-byte.
Proof size: 3 102 bytes. Avg time: 102.0 µs. Verifier root hash: 95aa74708738c5254c706bbca3245b520022aad949674822218094bca15671b6.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (6 layers, AggregateSumOnRange collapse at the bottom) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVHash(HASH[a17138f666ae4ab19c1c2930ad94c7e29a6a82789398fc4d3a0b053d3499ac68]))
2: Parent
3: Push(KVValueHash(sentAt, ProvableSumTree(800000000000ffff, 550000), HASH[3b3f5bf4e079c639895d84f8c5003fe135ccb46bf81b29fd3bdf415cddffe45c]))
4: Child)
lower_layers: {
sentAt => {
LayerProof {
proof: Merk(
0: Push(HashWithSum(kv_hash=HASH[a133f55e1f33e2b3a2a78f9f89c3aff8fd06f0ad25254490a42beef2c130e9dd], left=HASH[d39ec024ef5f61890206e3c6d29638a77b94159869d3f7f2ff1941f2f5e87e25], right=HASH[620ccd98c998cb3a503ee12098a6617743a99ffeb644069927ce7ebd8b70f76a], sum=180208))
1: Push(KVDigestSum(0x8000000000007fff, HASH[3e1b6b140f2f162e572401ab79584a57b7e6bd9aef2a3a11ffc94f983ed1e29e], 360430))
2: Parent
3: Push(HashWithSum(kv_hash=HASH[fe76ca504e6930ba89fde3579cc12e8b087398131edac88ced34c92dc89ad94b], left=HASH[f9636ca299ccd678c7e680dfee0ee20cfc97e7205ef085c0ca543f28b1cb5153], right=HASH[dd2ed3a63ec9da0bc56beae8bd1ce63e3b5873e4ae91105172a72c35ffab89d5], sum=90110))
4: Push(KVDigestSum(0x800000000000bfff, HASH[85990c6978e9933c4c27bfebd6ad8b14293dc3dba72803a7d6052dbeb40fef62], 180214))
5: Parent
6: Push(HashWithSum(kv_hash=HASH[9464d86ed7482bea742921278b684b6b448c0c25353858d1d84d24bb99638373], left=HASH[1108c69f23ffac356496bc7bce37d06859d70fb1f4b8cbb7a1d87980919d4a61], right=HASH[680cdc0f17073459247d5782e373245d2247471f6fbfb600a33f71e8a1ab5480], sum=2808))
7: Push(KVDigestSum(0x800000000000c1ff, HASH[04751262de120fb4c72492011ca10e081eb327ef49262fd03101d51c2f4649e6], 5622))
8: Parent
9: Push(HashWithSum(kv_hash=HASH[4c90aeb3a471fc6dd7f702c31133add5bacf8ad1e463d38b31bc9bee8a7b9c06], left=HASH[b8bde476ff8cb68fd463ba2834b30e2715c3bc6f95fa7b9cc93962b91aa6e6a5], right=HASH[63e03a72eed0862e13fedd63be7e1db08f66ef141a3d6071e38c66bc08504374], sum=1410))
10: Push(KVDigestSum(0x800000000000c2ff, HASH[83fdac1310ddca8772a2f40e5cf1de0834d48ea2ac41832a9bb0893d8cf96464], 2810))
11: Parent
12: Push(HashWithSum(kv_hash=HASH[b7afd069ba319e087f24a7d84c05eeaee3676f751c8af09f62cda8966811ea3b], left=HASH[dc4cef21a317ba562e12fcd541a9f645d027ac9323e69868b82cf36625afef43], right=HASH[051f312ccfcbb07b2b99e78636b11050724c2a573ef8c257e54a3130c5501f00], sum=336))
13: Push(KVDigestSum(0x800000000000c33f, HASH[9a94706e02ff4e42f2d7beb30a0f7747b5b31503a25f3c82be7136ef41f39433], 688))
14: Parent
15: Push(HashWithSum(kv_hash=HASH[4628fb01275e90713dacc4cb940b5c7ca5bf43690f1aee6a34bdf51114f016ef], left=HASH[11160330576883ac71e4842bc0ad1c73d28088b2ea54406c3f519e820ed5556f], right=HASH[73214eec9c3e28b1dd0e4b930e134c536d484f7ef3b64144e59a178c1bc375e8], sum=90))
16: Push(KVDigestSum(0x800000000000c34f, HASH[c1ec8175bf1c39fc6543d151ee1d0f3663b80511139e486c5e21ef248cd291bd], 170))
17: Parent
18: Push(KVDigestSum(0x800000000000c350, HASH[b22d3790d948924dc6311518f2872b53c0590d3cc497d7a653f1ce7b9923e499], 1))
19: Push(KVDigestSum(0x800000000000c351, HASH[af12fe5f400e96dc6517b898330583638bd8fe091ff8ee989a43b10848fe5f60], 6))
20: Parent
21: Push(HashWithSum(kv_hash=HASH[3fbfe732d4eb60c1611bf1658d092de8767129cb7a5bb688b02251d134039e11], left=HASH[0000000000000000000000000000000000000000000000000000000000000000], right=HASH[0000000000000000000000000000000000000000000000000000000000000000], sum=3))
22: Child
23: Push(KVDigestSum(0x800000000000c353, HASH[00536d93be0133a750702ef0ab5207da8c246fbb9ea731a74db9ac94884e919a], 28))
24: Parent
25: Push(HashWithSum(kv_hash=HASH[5f32751618bcf16785ecfd6e051158b4ebd6ed43432ffe44c4f4d26abf91524e], left=HASH[8ee930cb026de29ec8d652bba6b0644c7aed8bedec2da781805635aaeb61453c], right=HASH[d3d6c02c771eeac87ea43855db8535bb5f7e380046eb0089c7a52856bf120475], sum=18))
26: Child
27: Push(KVDigestSum(0x800000000000c357, HASH[c43d06166234e25f98140da5c573120a4ba81679559bb1876a71efbcabbbe459], 70))
28: Parent
29: Push(HashWithSum(kv_hash=HASH[cee1be881b3ee6049936fd08f6d6080d30209d400a009b8007a99be89c96002d], left=HASH[9f25c5bd27c6ec98ef1f74e31413413c76dd011377a07775817dd9d4f31bd95f], right=HASH[6896b09cfa0768780de9464637f284406ddc7c45e47f562518bf093876aa9a0f], sum=34))
30: Child
31: Child
32: Push(KVDigestSum(0x800000000000c35f, HASH[0bee916b5ca009e480f594b34b4ff0023e003a520b9ae625beb84c53ab3bc93b], 348))
33: Parent
34: Push(HashWithSum(kv_hash=HASH[4ca53cefcf67c600b1c36334925073e1fcb1a3500dbc558428cb8476de92ba37], left=HASH[0f3cc2f97ad17f6ea81ea17d915947fdd47e4e20ccd1d2ff4bd4b81a22515938], right=HASH[4796a3bc0d36b5277ace3d37c112f6f5b6cb5945b1d5cc72bf7efec7bf42d987], sum=172))
35: Child
36: Child
37: Push(KVDigestSum(0x800000000000c37f, HASH[fcb1700a1b6be13cd9f3c3d80abde188701aa1d01b25c24929588a2f858eae9c], 1390))
38: Parent
39: Push(HashWithSum(kv_hash=HASH[0e51a96e8e17135a01dce39490c7f55c49024de520f2870f84eedce77a65ecbb], left=HASH[215439487b3dcef7c0186e740ff1f2da27f8973466615691d6c2dc5db2134417], right=HASH[4911eb7861bbc7ea19c572eb9b76900bc2e0f80fbf178c131b30f6ca8969852a], sum=694))
40: Child
41: Child
42: Child
43: Push(KVDigestSum(0x800000000000c3ff, HASH[c875d92f9ae9adb882f612df44549ea07539a0d25fa915f7831a9d64993627a1], 11262))
44: Parent
45: Push(HashWithSum(kv_hash=HASH[3d4a96f576bd6fa7bf10947e8b6c3884306d374d499ab1f4b510d1d13c2b718b], left=HASH[9c4dc81fa4334b80fb36993450bc59dd16d9eb430e89a55e22678dd7be2c290b], right=HASH[db362866db8cdd17bc56f89f9f920895122e4bcb41b8897bac822d8895c520cc], sum=5634))
46: Child
47: Push(KVDigestSum(0x800000000000c7ff, HASH[277f41ec61fbf2c324169b989c07e1bbae573df54f10c0dce1220daf0ccd9b7d], 22520))
48: Parent
49: Push(HashWithSum(kv_hash=HASH[eeae2cee7b1a1141fbda7f7bce1062e969781102c4387ac4075e5702e7c1f608], left=HASH[9b7d2fd9115f3e799edc4c8bbfe0bca3b29ef9adad6c179984cbe2f632de949b], right=HASH[be02ecdf09c0b45fcd3246cb641fa7afa97bf0b1d10963aa337b93ead23cd74d], sum=11248))
50: Child
51: Push(KVDigestSum(0x800000000000cfff, HASH[e7d6b0ffd061d780d54d7d86cf76c401f35df152d58a362fe2129128aeb75fcd], 45048))
52: Parent
53: Push(HashWithSum(kv_hash=HASH[a641b281dca0faa95010eb5705db941b38c8008b38669c14e33ead3b9c341cf3], left=HASH[ff32269c72ee6af174b05a20db06bf57d766b05684208ff2cf676d0fa5e4b9d8], right=HASH[283d35cdf0761e087db90a9e8dfc9f816db0d3782125e8a3087f3e058b4a5ebb], sum=22520))
54: Child
55: Push(KVDigestSum(0x800000000000dfff, HASH[f3cd6aa655507ab09ee96baa7e1f951324ca052bfc39f14bd1f9443a6b8fc082], 90102))
56: Parent
57: Push(HashWithSum(kv_hash=HASH[5a331ce6ce57978d72fd13599ce6cb2e850c68c206e61941cfd374d6de22c514], left=HASH[b0acdc1413331ec55ef36da32fb0906e6dbc800d9ddb12041ab8ff9370d0bcf2], right=HASH[2753ae0214b4f860dc66dd4644f673f5fd48c13e483bd7088bbbcdac9774c5a0], sum=45050))
58: Child
59: Child
60: Child
61: Push(KVDigestSum(0x800000000000ffff, HASH[3326597e4515f458511d3ed24b5e50ce0b085821e1825d772fc13b500c0250d5], 550000))
62: Parent
63: Push(HashWithSum(kv_hash=HASH[576fea2411d44fafd6be01500f5fcca777ed0233b0d64577631c32f60119beed], left=HASH[f1119f240cf23919e3575090cb4b8999d5b9c6f58b24e0b20ac2b1837b9ec16c], right=HASH[00f34e547329b810797b6c891da04ac6bca733adea73b53eb0696acac942d413], sum=189564))
64: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. Layers 1–5 mirror Q3/Q6's bySentAt descent. The bottom layer (layer 6) is structurally distinct from every previous query: it contains no KVValueHashFeatureTypeWithChildHash terminator ops at all. Instead the proof reveals only the merk-boundary structure of the range (HashWithSum for opaque subtrees that the range straddles, KVDigestSum for boundary kvs whose subtree-sums the verifier needs to sum-up). The verifier's range collapse works by traversing the merk boundary and accumulating the sum fields on HashWithSum / KVDigestSum ops for nodes the range covers, producing a single aggregate i64 = 274 999 along with the recomputed root hash. The op-numbered running sums in the proof above trace the merk's binary descent: op 0's sum=180208 is the merk root's left subtree (= 180 214 minus the boundary kv at 0x…7fff with sum 360 430 partially adjusted), and op 61's KVDigestSum(0x800000000000ffff, …, 550000) is the full-tree's max-sentinel committing the full timeline sum 550 000. The RangeAfter(serialize_value_for_key("sentAt", 50000)..) query item picks out the right-half subtrees from sentAt = 50 001 forward; their cumulative sum is 274 999. Verified via GroveDb::verify_aggregate_sum_query, root hash 95aa7470…71b6.
This is the headline payoff: 49 999 matched timestamps, zero documents materialized, a single committed sum verified in O(log T) bytes — same proof-size profile as count's Query 7. The proof is slightly larger than Query 8's 2 657 bytes because bySentAt covers ~100 000 distinct values (the full timeline) versus byRecipientTime's 1 000 distinct sentAts per recipient — wider merk-trees mean a deeper descent + more boundary commits in the proof.
Diagram: per-layer merk-tree structure
Layers 1–5 are identical to Q3/Q6. Layer 6 is where this query diverges sharply from the point-lookup queries: no individual leaf is the target. The "target" is the range collapse itself — a single committed sum the verifier extracts by walking the boundary and accumulating sum fields.
flowchart TB
subgraph L6["Layer 6 — bySentAt ProvableSumTree (RANGE COLLAPSE, no individual target)"]
direction TB
L6_collapse["<b>AggregateSumOnRange(sentAt > 50000)</b><br/>verified sum = <b>274 999</b><br/>(49 999 timestamps covered, each contributing<br/>amount = (row % 10) + 1 per the fixture cycle)"]:::target
L6_boundary["Range-boundary commitments (64 merk ops):<br/>HashWithSum for opaque subtree commits the range straddles<br/>(each carries the subtree's running sum_i64)<br/>+ KVDigestSum at boundary kvs where the range partially covers<br/>(e.g. op 18 KVDigestSum(0x…c350, …, 1) is the floor-exclusive boundary)<br/>+ op 61 KVDigestSum(0x…ffff, …, 550000) is the max-sentinel<br/>full-tree commitment, used by the verifier to validate completeness"]:::pst
L6_collapse --> L6_boundary
end
classDef pst fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The cyan node here represents the aggregate output, not a single revealed kv. That's the structural payoff of rangeSummable: true: the merk-tree's per-node i64 sum fields turn a potentially N-leaf walk into a constant-depth boundary walk that produces a single verified sum. Q7's sum = 274 999 is byte-for-byte recoverable from the 64 boundary ops above without ever revealing the 49 999 individual sentAt leaves.
Query 8 — Compound == + Range (byRecipientTime)
select = SUM(amount)
where = recipient == "recipient_050" AND sentAt > 50000
sum_property = "amount"
prove = true
Path query:
path: ["@", contract_id, 0x01, "tip", "recipient", "recipient_050", "sentAt"]
query items: AggregateSumOnRange(RangeAfter(serialize_value_for_key("sentAt", 50000)..))
Verified result:
(root_hash, sum) where sum = 500
(Per recipient_050, 500 of their 1 000 tips have sentAt > 50 000. Each contributes amount = 1 per the per-recipient cycle described in the fixture narrative, so the sum is 500 × 1 = 500. For a recipient with amount = 10, the same shape would land 500 × 10 = 5 000.)
Proof size: 2 657 bytes — the only end-to-end-verified AggregateSumOnRange proof on this fixture today, since Query 7 is blocked on the top-level promotion bug. Confirms the carrier path through byRecipientTime's NotSummed(ProvableSumTree) continuation works correctly. Avg time: 91.3 µs.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (8 layers: 6 path layers + 2 byRecipientTime continuation layers, range collapse at the bottom) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVValueHash(recipient, Tree(000000000000003fffffffffffffffc000000000000000000000000000000000), HASH[f5801a1723ac6dfd5ff650eaa97d8b134255eef3ab8429dd516690dcfac74221]))
2: Parent
3: Push(Hash(HASH[143e80400b4fe5e0de4201bdf02853ac92347483a4a559040aa4ccb1ff4e3b03]))
4: Child)
lower_layers: {
recipient => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[b8804ea3f7998def339db7cadd6a6b29ba5bfadbdfa15b67802ef52e42adb68e]))
1: Push(KVHash(HASH[3056a7c2daf102d31d0f461d45e661303b1562311971debbf15f40938727a074]))
2: Parent
3: Push(Hash(HASH[c406363887b632d071f2ee6ed83df682aace9a5456997aa4f4b944576476fbb6]))
4: Push(KVHash(HASH[6b8ef1df5ba1299cd5b605dc7642be2ffb8356fd7f51c3a0498b6b657cddeebc]))
5: Parent
6: Push(Hash(HASH[347abc0b69e504bc619e9549e509c21be3c0ad1c9e03d8fb34bb5ed07e5cd26f]))
7: Push(KVHash(HASH[ff9b5006130777d589b77e6f5bdda0f86879daace181cdc8e3d97bc3718e0a48]))
8: Parent
9: Push(KVValueHash(0x0000000000000032ffffffffffffffcd00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[264c6aac1a1acd864832a6f25ac42c626afb95dc79ac8c233d2b05c6df048f1c]))
10: Child
11: Push(KVHash(HASH[5159e1ccad8c4ed1bbf7f52ec34e9ab4ee108559194e39b1af78cf42866b32cc]))
12: Parent
13: Push(Hash(HASH[4317777613ab1a0d6966670195ccce83dee1031fd37013c9ce625c016d1d6d17]))
14: Child
15: Push(KVHash(HASH[24f6646d8b75a6a428d05589c1cc1e80f1e52900924710ff6eafe9da0edc73d0]))
16: Parent
17: Push(Hash(HASH[14f8282bd0b5d9a8e7e471d3cfd215f02f5be2cfbf837c5e938c2871a2eddcb9]))
18: Child
19: Child
20: Child
21: Push(KVHash(HASH[cea6360efbf77b38d2ea206d508ea03c0d92fe7c02c5d9176aa322e8263a3acc]))
22: Parent
23: Push(Hash(HASH[209f3325816d5f02a1647311a4f1d9c68fcbacf1540be9b5ae65413f58ca3b26]))
24: Child)
lower_layers: {
0x0000000000000032ffffffffffffffcd00000000000000000000000000000000 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[ad21cf215639bca21e163333196de5b1774e0e91bf266a323c3a0b78c8cf7998]))
1: Push(KVValueHash(sentAt, NotSummed(ProvableSumTree(800000000000c7ce, 1000)), HASH[fbe3df47d0e3ee0dbdb9a1491fc05d93ae26f8cb914fb240a42e9989a3752cbc]))
2: Parent)
lower_layers: {
sentAt => {
LayerProof {
proof: Merk(
0: Push(HashWithSum(kv_hash=HASH[8a0594fb82ff688d061192d70597c544bc56c18badc2ba8e3ce83372adefa71a], left=HASH[04ea853c427ca0c50abed8b5e6349be94cf55f8988f1e6f314d5b15db57fc4e4], right=HASH[6c6832bf2d87daad24055a9195deeadfe50d4c1b816c79b1bbc6ec212ff94d62], sum=255))
1: Push(KVDigestSum(0x80000000000063ce, HASH[7e46f009482a37f9e01a1a251291ae5bad2d1ffd6e04f1f88b44853c40373401], 511))
2: Parent
3: Push(HashWithSum(kv_hash=HASH[f9ac98fc04f30b63ec49c758c3d8fe7f3657182dab7d6255271d3540c521c3e1], left=HASH[c76899568ea6f6268413bd5c0096ea1a822c60c0e8e8d2ec5738b56e6a4cefed], right=HASH[953541994578d66b8d0d3ac32bef836f3c1d1a0f7a1e55cf99c5312df9f170c4], sum=127))
4: Push(KVDigestSum(0x80000000000095ce, HASH[0e67180c6b3c0ff547de58f5c0b36d63fea5499b4146ebe315825a43590b9e53], 255))
5: Parent
6: Push(HashWithSum(kv_hash=HASH[d7245bd11393b60e38a551ae083e703e4a2584b6eaa70c1389934ae2fcb5f593], left=HASH[165d0e36dcabfbc3a1890a603eedfa51e0e170de6a6961f949031005746ac325], right=HASH[3aefae9c66dfb5bcbfbed5c59816a7ce7b447c7a9432d130e2b42a3c19c0d3d2], sum=63))
7: Push(KVDigestSum(0x800000000000aece, HASH[d27cc74018d6d49e895ad0db3e93d0dd05619e0efa0bb50ffc6691f69a98aa90], 127))
8: Parent
9: Push(HashWithSum(kv_hash=HASH[dc56bbcb92f002e0a7e223cd2860a09121f117de4bcd725676d5b1ff48528d9f], left=HASH[eddcd28c36db5ecef8838d42845fee17a2087e0831ff6813ea2d38f3eaf1443f], right=HASH[bca8a2fc6c5a4c5025a65d845a5b54ac15bbfbbd3aebbba95c9dc38d0e94d3bb], sum=31))
10: Push(KVDigestSum(0x800000000000bb4e, HASH[89151daf6d33cc9634cc0bcd93ffd0ab0be5aa9d4ddac159aa383b589432d14d], 63))
11: Parent
12: Push(HashWithSum(kv_hash=HASH[749de6348d21966ee040946142c7e4dce7b113041032dc9cc95d4c0e72bacdc9], left=HASH[32eee9d294a45839a77c1a0d6c05fa3bbebeb0435b4894409bb5863ac7e050d2], right=HASH[0b251e9efd6d616d3f9c5e361a5032b24d4526b556e16ee04a02984d9aa002d0], sum=15))
13: Push(KVDigestSum(0x800000000000c18e, HASH[fa15c265143026dd1e34dbc5b986ba9839069a2c01692712954beb492a096d0e], 31))
14: Parent
15: Push(HashWithSum(kv_hash=HASH[6f8df1c4654e54ea20cd4dad686fc471cb8f94ee6cfc7c4a72bd00792b594138], left=HASH[3c80faf26e8f2a6a266234f2367a0ce908926b72a4092fc7c3b02bebe1c47e85], right=HASH[390d9a0a56902ab7b8611d68e07f0068667302802b4bcffe71796c01d0a0ccae], sum=3))
16: Push(KVDigestSum(0x800000000000c31e, HASH[110023b15d77b39d67044847913b994adf94cdb8ecfa5bce2abd4d37d6ac68c7], 7))
17: Parent
18: Push(KVDigestSum(0x800000000000c382, HASH[1ea127fb3003e8de7bcfbe341b29da4cff8b8c119dfa10d47eaca70fb7a62d54], 1))
19: Push(KVDigestSum(0x800000000000c3e6, HASH[35c7ea9920ba64ff253a6e16dd8ed0a38231221d2bdd6e769c35e96634ae994b], 3))
20: Parent
21: Push(HashWithSum(kv_hash=HASH[722fcc58984fccf6edb3dc0bb7bda4f6e35c57981fb6f70a937df696c079893f], left=HASH[0000000000000000000000000000000000000000000000000000000000000000], right=HASH[0000000000000000000000000000000000000000000000000000000000000000], sum=1))
22: Child
23: Child
24: Push(KVDigestSum(0x800000000000c4ae, HASH[23944bfe2e2a1e79e00e267e0ab5ff93748f8e7273049e02f91dbf97892ccbdb], 15))
25: Parent
26: Push(HashWithSum(kv_hash=HASH[b59968c7e2c22578ed837d29716b533315b0a1717de0a3165126829aeeac00fc], left=HASH[1c2fcdb483a1ed6e433b0c42056c20b68502f6ac2f074daa3335cd3508d2b058], right=HASH[ecf421d6c9e3e68bafef2663530a1a87339ed7b7cd66539991d9c32051dc47f1], sum=7))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVDigestSum(0x800000000000c7ce, HASH[978baee06712b4ec8bcb7c36e140dc5c4031e2d68b3aa15c2167870a2a45778a], 1000))
34: Parent
35: Push(HashWithSum(kv_hash=HASH[db8a2deb3aa116b1e0d5a1de03b8c4deed54d2ab06ad693238424a7a68288008], left=HASH[f58ece054ce9b084e4042e0ee2e03c8dddde53f1936c94e733c4ef5c146c597e], right=HASH[18f3c1450fce7a85cf5eb87ccc2c4008ef350b18d72de000f2e0606788862659], sum=488))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. The first 7 layers mirror Q4's compound prefix descent: root → @ → contract → 0x01 → tip → recipient → recipient_050 (with the NotSummed(ProvableSumTree) continuation pointer at layer 7). Layer 8 is the AggregateSumOnRange collapse on byRecipientTime's continuation merk-tree — same structural shape as Q7's bottom layer (HashWithSum opaque subtree commits + KVDigestSum boundary kvs), but covering only the 1 000 sentAt entries under recipient_050 instead of the global 100 000. The verifier walks the merk boundary and accumulates sum fields for the right-half range (RangeAfter), yielding sum = 500 (recipient_050 has 500 tips with sentAt > 50000, each contributing amount = 1). Note op 33's KVDigestSum(0x800000000000c7ce, …, 1000) is the max-sentinel committing recipient_050's full 1 000-tip sum; op 35's HashWithSum(…, sum=488) is the parent subtree commit on the right of the range floor. Verified via GroveDb::verify_aggregate_sum_query, root hash 95aa7470…71b6.
Two-prefix descent (recipient_050 → sentAt) followed by an AggregateSumOnRange over byRecipientTime's continuation ProvableSumTree. The same O(log T') range collapse the unfiltered range query (Query 7) is intended to use, just with a compound prefix paid for at the boundary subtrees on the way down.
Diagram: per-layer merk-tree structure
Layers 1–7 mirror Q4's compound-prefix descent (recipient → recipient_050 → sentAt-continuation). The divergence is at layer 8: instead of a single absent terminator op, the proof presents an AggregateSumOnRange boundary walk that collapses to a single aggregate sum.
flowchart TB
subgraph L8["Layer 8 — byRecipientTime sentAt ProvableSumTree (RANGE COLLAPSE under recipient_050)"]
direction TB
L8_collapse["<b>AggregateSumOnRange(sentAt > 50000)</b><br/>verified sum = <b>500</b><br/>(500 of recipient_050's 1000 tips match,<br/>each contributing amount = 1 per the cycle)"]:::target
L8_boundary["Range-boundary commitments (37 merk ops):<br/>same HashWithSum + KVDigestSum shape as Q7's layer 6,<br/>but on a 1000-leaf merk-tree (recipient_050's per-recipient<br/>continuation) instead of the global 100 000-leaf tree.<br/>Op 33 KVDigestSum(0x…c7ce, …, 1000) is the max-sentinel<br/>(per-recipient max sentAt). Op 35 HashWithSum(…, sum=488)<br/>is the parent commit on the right of the range floor."]:::pst
L8_collapse --> L8_boundary
end
classDef pst fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
Q8 is smaller than Q7 (2 657 vs 3 102 bytes) for exactly one reason: the bottom layer's merk-tree is the per-recipient continuation (1 000 distinct sentAts under recipient_050) instead of the global timeline (100 000 distinct sentAts). Shallower merk-tree, shallower descent, fewer boundary commits. The Q7-Q8 byte delta is the structural cost of taking the global bySentAt path versus a compound-prefix descent.
Query 9 — Carrier-Aggregate (In plus range)
select = SUM(amount)
where = recipient IN ["recipient_000", "recipient_001", ..., "recipient_099"] AND sentAt > 50000
group_by = [recipient]
limit = 100
sum_property = "amount"
prove = true
This is the carrier-aggregate sum shape: an In clause on the index's prefix property combined with a range on its terminator, returning one sum per resolved In-bucket rather than a single aggregate across all matches. The group_by = [recipient] (not [recipient, sentAt]) routes through SumMode::GroupByIn — the routing table at mode_detection/v0/mod.rs maps (GroupByIn, In, range, prove) → RangeAggregateCarrierProof, which is what the per-In-bucket aggregation needs. A group_by = [recipient, sentAt] (GroupByCompound) routes to RangeDistinctProof instead — per-(in_key, range_key) distinct walk, a different proof shape entirely. Sum analog of count's Range-Countable group-by carrier-aggregate. The primitive landed in grovedb PR #670 (head e98bab5f); the verifier is GroveDb::verify_aggregate_sum_query_per_key.
Path query (carrier-style: outer Query enumerates the In branches, subquery descends through the terminator's AggregateSumOnRange):
path: ["@", contract_id, 0x01, "tip", "recipient"]
query items: [Key("recipient_000"), Key("recipient_001"), ..., Key("recipient_099")]
subquery_path: ["sentAt"]
subquery items: AggregateSumOnRange(RangeAfter(serialize_value_for_key("sentAt", 50000)..))
Verified result (returned by GroveDb::verify_aggregate_sum_query_per_key):
(root_hash, entries) where entries =
[ ("recipient_000", 499 × 1 = 499),
("recipient_001", 500 × 2 = 1000),
("recipient_002", 500 × 3 = 1500),
...
("recipient_009", 500 × 10 = 5000),
("recipient_010", 500 × 1 = 500),
...
("recipient_099", 500 × 10 = 5000) ]
(Per recipient, 500 of their 1 000 tips have sentAt > 50 000, and each recipient's per-doc amount is constant per the fixture's per-recipient cycle — so each outer-bucket sum is 500 × amount_for_recipient. recipient_000 is the lone exception: the boundary timestamp sentAt = 50 000 happens to be one of recipient_000's tips, so only 499 of recipient_000's tips satisfy sentAt > 50 000. The bench's verified entries lead with ("recipient_000", sum = 499).)
Proof size: 169 064 bytes (k=100 outer buckets). That's significantly larger than the non-carrier In shapes (Q5 12 064 B for k=100, Q6 9 784 B for k=100) because each of the 100 outer buckets walks an AggregateSumOnRange proof, whereas Q5/Q6 only commit a single-element value-tree sum_value per branch. The per-bucket marginal cost is ≈ 1 690 bytes (most of which is the inner range proof — the carrier composition itself adds only ≈ 100 bytes per outer Key versus the equivalent flat-In shape). Avg time: 11 507.9 µs (k=100) — ≈ 115 µs per outer bucket, consistent with Q8's 91.3 µs single-bucket AggregateSumOnRange plus the carrier descent's per-outer-key cost.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (206 LayerProofs total — recipient_000's full descent shown; recipient_001..recipient_099 abbreviated for readability) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[2f2bb4914c2a17715b3084357a87925d56371820af54019ed45f53b0f2ff352f]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b1ed2f146918bb1d0f6fafa85f17558e030a4337c9cb85d9364da64ae1801ec])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[c53ef5d17d01aba0f72670d88a09905db45e8639ffe72a77ab68e00770b1334b]))
1: Push(KVValueHash(0x01, Tree(746970), HASH[fba96529e5faf688cd9f3544b9f7979fde626476f4022e4cee50138ceb0295d2]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(tip, Tree(726563697069656e74), HASH[0fe5cf57426e356c1cfcc44a0c35c1dabf78aebdd7ef26d070950a2c7ff86800])))
lower_layers: {
tip => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[15fd7f83e57e9b83533d5df4eacabf0e99a861c6cc59a539b8f486a02414babd]))
1: Push(KVValueHash(recipient, Tree(000000000000003fffffffffffffffc000000000000000000000000000000000), HASH[f5801a1723ac6dfd5ff650eaa97d8b134255eef3ab8429dd516690dcfac74221]))
2: Parent
3: Push(Hash(HASH[143e80400b4fe5e0de4201bdf02853ac92347483a4a559040aa4ccb1ff4e3b03]))
4: Child)
lower_layers: {
recipient => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x0000000000000000ffffffffffffffff00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[2c932396123fe6bae5fa3e4fa42225852e08a2dcf7600991e79fb9347de7506e]))
1: Push(KVValueHash(0x0000000000000001fffffffffffffffe00000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[43c65eed82b8e5d08b7fea673bf120a82332adbffb8582e0f8a3205190fab720]))
2: Parent
3: Push(KVValueHash(0x0000000000000002fffffffffffffffd00000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[e4cf97fee88b23d122998ea98dbc83fc662da5e0d80c1f854758b2ce56699289]))
4: Child
5: Push(KVValueHash(0x0000000000000003fffffffffffffffc00000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[b552d6302d6bd0f76277a1b56423f25efec67612d42c924e0d486202f31e3e44]))
6: Parent
7: Push(KVValueHash(0x0000000000000004fffffffffffffffb00000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[01be3636585f11d5a09875214cb8e7c68cbe462c739f012693ae3404c58f1179]))
8: Push(KVValueHash(0x0000000000000005fffffffffffffffa00000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[2cd10cc36cb8056b17df3d117b6687d9bd857a79d1a73f2b85290d87a9ce4a24]))
9: Parent
10: Push(KVValueHash(0x0000000000000006fffffffffffffff900000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[f020781d83a04f027649d42a0dc92f91506ae35765844773f72e59071cb1f107]))
11: Child
12: Child
13: Push(KVValueHash(0x0000000000000007fffffffffffffff800000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[9fba5594f7f0b758415ae7159919102c1a2c8aecc98af22c36da32d83199f475]))
14: Parent
15: Push(KVValueHash(0x0000000000000008fffffffffffffff700000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[64188af510ecd884d09350ae08a5f42d5922321408aa552ec9af6c847d264a7e]))
16: Push(KVValueHash(0x0000000000000009fffffffffffffff600000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[304dc0b23c2eac4c2e8cf7eddd7b3c57bee411ae03ce59b9248c0b5ad89db242]))
17: Parent
18: Push(KVValueHash(0x000000000000000afffffffffffffff500000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[97b584dfeb399a5278c736c1c6668c12765926780a15e8e93678903734c61aa3]))
19: Child
20: Push(KVValueHash(0x000000000000000bfffffffffffffff400000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[76b3db0ddb6b2c5756cbc4d66cd37109501665ef122f6d1ce20c4a5566a0bf25]))
21: Parent
22: Push(KVValueHash(0x000000000000000cfffffffffffffff300000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[a81ae2020176545244f9cc4ef9b86f014802b6d11908985f38389f85b4222377]))
23: Push(KVValueHash(0x000000000000000dfffffffffffffff200000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[d46254b6791a7c20ebcb43b2c13fbd73a07704cce56bc78357719d48ae27d1ef]))
24: Parent
25: Push(KVValueHash(0x000000000000000efffffffffffffff100000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[1e8cdba5aa96432bc4a5cd1bb4f2808f2c48debdc450d7f878076217b3fd38aa]))
26: Child
27: Child
28: Child
29: Push(KVValueHash(0x000000000000000ffffffffffffffff000000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[62d0fc12b138b76a1686f65ee269cbe91156aeb3da7b50dd3fd908eb1e6bf9c2]))
30: Parent
31: Push(KVValueHash(0x0000000000000010ffffffffffffffef00000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[c43bd23a1c31c624127621e4480ca21d5b2a03725035d5b74301f6780122af92]))
32: Push(KVValueHash(0x0000000000000011ffffffffffffffee00000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[b9d2ff1ba31cc5ab8293e2a2c14609bf99adf6acc262c02801c15b04765c1857]))
33: Parent
34: Push(KVValueHash(0x0000000000000012ffffffffffffffed00000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[9fbbfcf2a78087d8f112050fa354e424f897638b570dc7afc82bf286a48aa2bd]))
35: Child
36: Push(KVValueHash(0x0000000000000013ffffffffffffffec00000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[929742b49a2ccf0ab75af768d9f37ac226212d4170f190e58f9f07f3e59b1500]))
37: Parent
38: Push(KVValueHash(0x0000000000000014ffffffffffffffeb00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[5131ff85f089920a94f6a9bf54f7db8bc753aa723485ae73f719acfb8992af87]))
39: Push(KVValueHash(0x0000000000000015ffffffffffffffea00000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[7a29693947430c79f6ecbd83cecc684e696b4303bdbbc110998408f57e72b6ac]))
40: Parent
41: Push(KVValueHash(0x0000000000000016ffffffffffffffe900000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[8b1b865f2a8118504ac42fbab6c2b621f155043eceb8fbc65b1ac60d698a2dd3]))
42: Child
43: Child
44: Push(KVValueHash(0x0000000000000017ffffffffffffffe800000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[3f98dbb6d76a0eff5dd84171dc4b610ce2e4148f0dd2347a064eff194ed73878]))
45: Parent
46: Push(KVValueHash(0x0000000000000018ffffffffffffffe700000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[1e7835c334b773fff98899fcdeb173516c5f61640e96bbc54a9b2347b3496592]))
47: Push(KVValueHash(0x0000000000000019ffffffffffffffe600000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[c77112c339a35d47922a4618d08315dab1a0d3cc2675d3416f20be077beffff6]))
48: Parent
49: Push(KVValueHash(0x000000000000001affffffffffffffe500000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[fa5fb0940cf60976030fa3ed1b215f2b8685bf61d5df0b1d071a0055a60b8669]))
50: Child
51: Push(KVValueHash(0x000000000000001bffffffffffffffe400000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[582775f7449c7c96988ea45a70744b17967586af24fc23254241961f04af89da]))
52: Parent
53: Push(KVValueHash(0x000000000000001cffffffffffffffe300000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[c71003aa7b862d8a1546fae82a4b0d40cd5bf00be0b3fa748295638262c6e97e]))
54: Push(KVValueHash(0x000000000000001dffffffffffffffe200000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[d200a08e3400945ec481e35843f5970c2040c5d649c0936249ae223373f66933]))
55: Parent
56: Push(KVValueHash(0x000000000000001effffffffffffffe100000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[e399c91063655e659638f034cb37aa38be8aa6591826afb8150168e23bab062c]))
57: Child
58: Child
59: Child
60: Child
61: Push(KVValueHash(0x000000000000001fffffffffffffffe000000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[6e393e0b7b898fae65df835b64af0060496d6b1f11e9eacbac8c3f5850136185]))
62: Parent
63: Push(KVValueHash(0x0000000000000020ffffffffffffffdf00000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[a39a5c32c8da4dd50b76788881dd61af87f88430984fcc1e3e3bda3f695060b7]))
64: Push(KVValueHash(0x0000000000000021ffffffffffffffde00000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[655cc67f6eb581596c76e146f9ce55294d53bfcbec54646afde11c7adaebdcbf]))
65: Parent
66: Push(KVValueHash(0x0000000000000022ffffffffffffffdd00000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[a13be8916a62ae0d1b924b74b5e5524f905ed3f5d980bb0be9d53fe224b0b089]))
67: Child
68: Push(KVValueHash(0x0000000000000023ffffffffffffffdc00000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[2bf66732319328bcf5588bef2b78e33a0d0881e5cec062fa0b87aeda98f6e64a]))
69: Parent
70: Push(KVValueHash(0x0000000000000024ffffffffffffffdb00000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[df49aa0199c39cd22de27d4df6ff98438eb3e3b07b851cc48efb30fbd381c8d7]))
71: Push(KVValueHash(0x0000000000000025ffffffffffffffda00000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[284d47e7e1d21fc8f37dac05a6deaf9f27506000ff5713c8936633174bacb660]))
72: Parent
73: Push(KVValueHash(0x0000000000000026ffffffffffffffd900000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[95bf9e3ea0a08e43d28e954311f6664532d015c8b5e1b0c0155327095aaf6858]))
74: Child
75: Child
76: Push(KVValueHash(0x0000000000000027ffffffffffffffd800000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[bf951ec3b345040d169c258847c11f19c20e1579f492fba4ef478fbddc6c1a7f]))
77: Parent
78: Push(KVValueHash(0x0000000000000028ffffffffffffffd700000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[55ff1a89b12b01d1e31b9f599dec325ea52ada71065011ed2601e6a53373ce6d]))
79: Push(KVValueHash(0x0000000000000029ffffffffffffffd600000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[5f38013b5c19b8272676fa4ad0ba58b3880b195d35b3d3ac2815b8233bf0de30]))
80: Parent
81: Push(KVValueHash(0x000000000000002affffffffffffffd500000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[0b443a9b31f304014595cdf5129f34da66e1a7274fc3548e8c799805232d3e23]))
82: Child
83: Push(KVValueHash(0x000000000000002bffffffffffffffd400000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[d349113a977fe2c1e5fdffe70a6fec8595da6fcea4882b63f2460516d141bc57]))
84: Parent
85: Push(KVValueHash(0x000000000000002cffffffffffffffd300000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[a198da76b8f9db38a4c80a602d8d2e9ac0ad7758ddb483450b4a3f13e3592479]))
86: Push(KVValueHash(0x000000000000002dffffffffffffffd200000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[33b369e7c75c3d3d362dbf33965d1b19dc96216d7723bebb78f10e49547a56b8]))
87: Parent
88: Push(KVValueHash(0x000000000000002effffffffffffffd100000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[051215e98dc597dd3beec177f691301fd0607916809ed00c36be278bd91a8e65]))
89: Child
90: Child
91: Child
92: Push(KVValueHash(0x000000000000002fffffffffffffffd000000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[0cdd2e9e49edccb1edd6fecb4f2d1d8c9f1355db935a67747974fbd803bb4d76]))
93: Parent
94: Push(KVValueHash(0x0000000000000030ffffffffffffffcf00000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[e9a1c2651036336e091b4ba6f373f616c8409d54ab2d8b8949778d930204c551]))
95: Push(KVValueHash(0x0000000000000031ffffffffffffffce00000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[74853523c4698c7e8cfa1f0f744a3da1480993f92f84805c34836cb0bfc31c26]))
96: Parent
97: Push(KVValueHash(0x0000000000000032ffffffffffffffcd00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[264c6aac1a1acd864832a6f25ac42c626afb95dc79ac8c233d2b05c6df048f1c]))
98: Child
99: Push(KVValueHash(0x0000000000000033ffffffffffffffcc00000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[b9cb74f5fabd2c1a81d0cf16213246acadcca2240d5910e4e2b22533d4f87067]))
100: Parent
101: Push(KVValueHash(0x0000000000000034ffffffffffffffcb00000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[257e1c20c28d5cf3bb2959b2c33e75ecbcf454188e2553200315693e7f2124bc]))
102: Push(KVValueHash(0x0000000000000035ffffffffffffffca00000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[738ae56fcfe64ed942f759be3089512fb256097dd246da488998d00c13bb55ee]))
103: Parent
104: Push(KVValueHash(0x0000000000000036ffffffffffffffc900000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[9badbc44201ecd712314ec0086d0d47224d5dfab25b95149aa2fedf39989644c]))
105: Child
106: Child
107: Push(KVValueHash(0x0000000000000037ffffffffffffffc800000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[167e2a54f09ffbf49d152a32371a8035596a3a9a2d839f055f46faa75ade51c3]))
108: Parent
109: Push(KVValueHash(0x0000000000000038ffffffffffffffc700000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[a447c9f23d474c77f7041c698f96a95aa8421d55e6fc7eee58f3b4be0d6caa9c]))
110: Push(KVValueHash(0x0000000000000039ffffffffffffffc600000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[ced5fe912b9e12105bf8a733df895ed8bdd75cce53033a6cb51e78fc361f67a8]))
111: Parent
112: Push(KVValueHash(0x000000000000003affffffffffffffc500000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[f1ec66f96298db46aeceaa1785dd24e479005237456bf8ee6482a8731cac4dd2]))
113: Child
114: Push(KVValueHash(0x000000000000003bffffffffffffffc400000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[f354580129aa8e8c9d0df8ce9842e72c198f4865101825aa64efaba200a18425]))
115: Parent
116: Push(KVValueHash(0x000000000000003cffffffffffffffc300000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[bc33576ad567e77c108ce92c1aafa1122c8642e8849e93fca8e91b8f6532bdf5]))
117: Push(KVValueHash(0x000000000000003dffffffffffffffc200000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[04bde39065544c84157f4c6248222af72678e1536735f870375c7e516d3f3fc4]))
118: Parent
119: Push(KVValueHash(0x000000000000003effffffffffffffc100000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[3bdadb83f854abf1c7aae1fdd86fd6e0c26753e2f813a1494f4ce6ee592371c9]))
120: Child
121: Child
122: Child
123: Child
124: Child
125: Push(KVValueHash(0x000000000000003fffffffffffffffc000000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[604318edaf54250e84ab7756467488be7cf9c6b3252942117fd36d6c53c2b872]))
126: Parent
127: Push(KVValueHash(0x0000000000000040ffffffffffffffbf00000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[c772aab9ebd16e076279e440adc1a8d365eefb36a6d25760a8fe0023b88b5904]))
128: Push(KVValueHash(0x0000000000000041ffffffffffffffbe00000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[448ae13230a21a21f7df14295673b3dd7337536868d9a667db5a756e37be845f]))
129: Parent
130: Push(KVValueHash(0x0000000000000042ffffffffffffffbd00000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[1468ec171f1365f54e1845009bbd9105e86b0f9288a9b0df4789270269558168]))
131: Child
132: Push(KVValueHash(0x0000000000000043ffffffffffffffbc00000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[173dc47f3e1fce81d4bdfffad37bb2fcd7e08923454a07aa5318ac23ace64b00]))
133: Parent
134: Push(KVValueHash(0x0000000000000044ffffffffffffffbb00000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[abd15b12cf6f035db4e163162e765da775cfd78012ce5d20eaad3976eb80b291]))
135: Push(KVValueHash(0x0000000000000045ffffffffffffffba00000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[ffa1ee3178182f5dc40a29afd3a236f8bba9892d1604d04114a32eb07c176f4f]))
136: Parent
137: Push(KVValueHash(0x0000000000000046ffffffffffffffb900000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[693437a92f6dfaee70b1cdb86fd6b208939bb431522a6f8321564f5f2e98b511]))
138: Child
139: Child
140: Push(KVValueHash(0x0000000000000047ffffffffffffffb800000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[bf2a9fb3100355e398e9c869997ac02f9e3ba95a143a08b052adb1ab82450bdb]))
141: Parent
142: Push(KVValueHash(0x0000000000000048ffffffffffffffb700000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[a5824c1c11fb02112dff6b46b3e450abfd4cdff94151dae513348691febf7bcf]))
143: Push(KVValueHash(0x0000000000000049ffffffffffffffb600000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[411d57a7e582d3f692ca98b3634e6aa75aba4681253d1bdb7144530a16bb546e]))
144: Parent
145: Push(KVValueHash(0x000000000000004affffffffffffffb500000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[e49198bc668baa08f93d66640ba7fdadfff9d59593f352bd628a3d6b57a2e53c]))
146: Child
147: Push(KVValueHash(0x000000000000004bffffffffffffffb400000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[c61be4cf68123d695433dc10c79d1600ff15306fd6123336b381f1918800434e]))
148: Parent
149: Push(KVValueHash(0x000000000000004cffffffffffffffb300000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[3a977675229ea451a61ac156f05fcbd91c85f08736666c27785513e98d0001e2]))
150: Push(KVValueHash(0x000000000000004dffffffffffffffb200000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[f4b3e29e2113370f153076839d6159d2d21149779ce88dadcceac6217347bf4c]))
151: Parent
152: Push(KVValueHash(0x000000000000004effffffffffffffb100000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[a98df638bc8e3148d9a7e407f4c7a352d9a6f7713487b6d3e4c5af09a78126d0]))
153: Child
154: Child
155: Child
156: Push(KVValueHash(0x000000000000004fffffffffffffffb000000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[80b994258a9d0e7500bbaeb910405395f7c7d03006fb7a15156d01740205a364]))
157: Parent
158: Push(KVValueHash(0x0000000000000050ffffffffffffffaf00000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[df44fba2712d3ba1d71ed1c1c586064197a41b7b903d3b5cae7f127c6c8a27e7]))
159: Push(KVValueHash(0x0000000000000051ffffffffffffffae00000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[2294f725ab5e79ad583cf60d7a942508fe292262b3fbfd3001aa916290a824d6]))
160: Parent
161: Push(KVValueHash(0x0000000000000052ffffffffffffffad00000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[79ca47b53c05f86e6c251af81b52999f032625e0c11eb0c06d41f2f3f83e1449]))
162: Child
163: Push(KVValueHash(0x0000000000000053ffffffffffffffac00000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[882db62836b8db91de36be15b3e85c22f30d2c7ffbf2357e4196104ea323713f]))
164: Parent
165: Push(KVValueHash(0x0000000000000054ffffffffffffffab00000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[2611abbb866d065a01f8d5dd51576365a84ff715b5cb9fd4b359f91393dd9b00]))
166: Push(KVValueHash(0x0000000000000055ffffffffffffffaa00000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[364d73402be79726b57efabf3758cf40784aeccfbc0dccee7692af12fe66c279]))
167: Parent
168: Push(KVValueHash(0x0000000000000056ffffffffffffffa900000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[22a362b3398c53a32b94e2c916a91a8b5d59c0bce9613658021a02f23896da19]))
169: Child
170: Child
171: Push(KVValueHash(0x0000000000000057ffffffffffffffa800000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[d8eb50c3ba501a828697da535d6a14665a12b9ea5e1214f37a738a27d7fe6ba1]))
172: Parent
173: Push(KVValueHash(0x0000000000000058ffffffffffffffa700000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[dada8ecba303e160deec29e7215e21572bdccc38647fd5550b0ac47af7ac52f4]))
174: Push(KVValueHash(0x0000000000000059ffffffffffffffa600000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[cbeaf7428892a5ca259437414d3eda7fd4de6ef55eb3ef0195f37fddd695e893]))
175: Parent
176: Push(KVValueHash(0x000000000000005affffffffffffffa500000000000000000000000000000000, SumTree(73656e744174, 1000), HASH[8320cbd66a3e07a381a989e260edf5b42579ea5951384c2280f037e698e95ff9]))
177: Child
178: Push(KVValueHash(0x000000000000005bffffffffffffffa400000000000000000000000000000000, SumTree(73656e744174, 2000), HASH[44342c04d0b5a3ede78cd4ef036bb3b48b8f9e4da2a502e0b5e36874cab62588]))
179: Parent
180: Push(KVValueHash(0x000000000000005cffffffffffffffa300000000000000000000000000000000, SumTree(73656e744174, 3000), HASH[547f96a911cb5f0657cad7e5ce0487fa1a8870b232c52ff3efac108e6978f9e9]))
181: Push(KVValueHash(0x000000000000005dffffffffffffffa200000000000000000000000000000000, SumTree(73656e744174, 4000), HASH[f8342da41347c35f4a966a1b9c4bbb684af653cd18c3bb200e630f2731266c06]))
182: Parent
183: Push(KVValueHash(0x000000000000005effffffffffffffa100000000000000000000000000000000, SumTree(73656e744174, 5000), HASH[18b8d60c015a057419a6405275cd02d490ff7cab627a653ec603a97757a5f49f]))
184: Child
185: Child
186: Push(KVValueHash(0x000000000000005fffffffffffffffa000000000000000000000000000000000, SumTree(73656e744174, 6000), HASH[d5e7eb06c7bd04852d0b25bd5df0039f3ef20d34711ef121f00cce3a31349dd5]))
187: Parent
188: Push(KVValueHash(0x0000000000000060ffffffffffffff9f00000000000000000000000000000000, SumTree(73656e744174, 7000), HASH[2462c14975dad4a210d556d0e579529e8a756b3b107cbbc75fe344b2a03b357b]))
189: Push(KVValueHash(0x0000000000000061ffffffffffffff9e00000000000000000000000000000000, SumTree(73656e744174, 8000), HASH[fe0d935c37c343340bc5f32dcedd4d3eb7c7cce20604065ff63bced0a951de75]))
190: Parent
191: Push(KVValueHash(0x0000000000000062ffffffffffffff9d00000000000000000000000000000000, SumTree(73656e744174, 9000), HASH[bbc5e27f1fdbba68e332d6cc50790017f29b2878b21e49b401701deee9bb2948]))
192: Push(KVValueHash(0x0000000000000063ffffffffffffff9c00000000000000000000000000000000, SumTree(73656e744174, 10000), HASH[15ca1dc935c384f6fd904a3de0643e4af7b8c38fe3946379c182a54714786ac1]))
193: Child
194: Child
195: Child
196: Child
197: Child
198: Child)
lower_layers: {
0x0000000000000000ffffffffffffffff00000000000000000000000000000000 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[f3cd8b8f77e6c9473ba47bb5650e481537027a91bcea62c04e70d9c658b7ce87]))
1: Push(KVValueHash(sentAt, NotSummed(ProvableSumTree(800000000000c79c, 1000)), HASH[92dc35643411662e66f2e8dc6b591cf7eed54f6dc4aaf69219c369c74188e8a7]))
2: Parent)
lower_layers: {
sentAt => {
LayerProof {
proof: Merk(
0: Push(HashWithSum(kv_hash=HASH[622ec7c875ff1016840e660f388f97bc899c32726d6f18a5966da84ec63ea355], left=HASH[ff984fa6039c70cdb6a896d7d69ecf3271dc0b103ee44d5d8301365c110b23fd], right=HASH[c90f5696ed4eb077cc0b1cdb904b2af2a75fd9b07f60345925cdddf7f6b38f2a], sum=255))
1: Push(KVDigestSum(0x800000000000639c, HASH[a9f7ac3993ee6e200d0f3d40697e009ce0bfe278009f77e3e82215ad7475c6e3], 511))
2: Parent
3: Push(HashWithSum(kv_hash=HASH[18cab59d8df3ddadeb0054a44540c64a916e022e3af157b38c175c7f9908b233], left=HASH[3f8f3e6a71375ccfae2714e6d9468ca8df8c914bcae7a5e67f2d167ccf82c6c6], right=HASH[fd4793282e7beeae5ffcc9ed6adf62ea213bbede0fc061ba5f5534303218a92f], sum=127))
4: Push(KVDigestSum(0x800000000000959c, HASH[94924e9e1cb6b1851911f3290f93a745bc1e9751044e5e462e702eb9c48f6572], 255))
5: Parent
6: Push(HashWithSum(kv_hash=HASH[f03986bdca555701a548ebb6483b177814570cc7e8638bb312b7c6939504eb18], left=HASH[f171da996938e7ea3d5840ea2b3448e40c701c3d941e2e81ea45450055f6e5c8], right=HASH[8eccc3e0d53a1d8dabf858f17886a204728adff59e19346d61d227fafa86e08a], sum=63))
7: Push(KVDigestSum(0x800000000000ae9c, HASH[a606c7eec7545c1124b0bccbcb29dedfdcba820f05446806c4eaf5c8106a667d], 127))
8: Parent
9: Push(HashWithSum(kv_hash=HASH[082aa2d5a4f27332961db4882ea673e1a9dc7c163eaf56c1ad0c2f876f8369c3], left=HASH[31d49f9b0d0c858fbe65650b5c4ffbfc86def44778a62253a2a81f92e60363dc], right=HASH[d52fc80e4e04177e9cab24953d10d870b73f5acd76f81076c784932b92f5cc4a], sum=31))
10: Push(KVDigestSum(0x800000000000bb1c, HASH[01f81f07f05bdab399b7f9b3583316cae8227ee8ef6f38f1292510e3fcdf8759], 63))
11: Parent
12: Push(HashWithSum(kv_hash=HASH[1ed335fe242c26d4530101e609a6d5a52a32dddfdd9fed225de10a7039d19238], left=HASH[373889c6db5863abb87ef61b3ee479d9ebf13922571c2f7e24a3e3b1eac7fa16], right=HASH[e126395fd037d9f94216301144e57dfa044de771c999597f6fcf5e604c2ce2f1], sum=15))
13: Push(KVDigestSum(0x800000000000c15c, HASH[7eb10dc2f4d81ce6c5df234cc54b19fd87b8b861fc5151dd0c19619171c7ce83], 31))
14: Parent
15: Push(HashWithSum(kv_hash=HASH[c59ef9246a6a7225fb2ef0558bf280f8df39ed99a17bb40812c639a18fd3af14], left=HASH[96e89928ae0de69be602723850619c0ebd3542da48ab32b8a1db5bc2e8603192], right=HASH[0cb2ba854b100015b09de5984a4103976d2d3b46900e9ddee76cf600234b5659], sum=3))
16: Push(KVDigestSum(0x800000000000c2ec, HASH[c68b52e5b8036d991946274166ef630ef19aeafd0f38d2388e2f7423cda1045f], 7))
17: Parent
18: Push(KVDigestSum(0x800000000000c350, HASH[b22d3790d948924dc6311518f2872b53c0590d3cc497d7a653f1ce7b9923e499], 1))
19: Push(KVDigestSum(0x800000000000c3b4, HASH[40b4e8cfba33cbdb2e6643b00f062c68ffd9786c156af78ba712b91a03af3ec0], 3))
20: Parent
21: Push(HashWithSum(kv_hash=HASH[38a6c1c68d6436d6059f514f0b532ffbb4d62788a72c283dea1f14e204399379], left=HASH[0000000000000000000000000000000000000000000000000000000000000000], right=HASH[0000000000000000000000000000000000000000000000000000000000000000], sum=1))
22: Child
23: Child
24: Push(KVDigestSum(0x800000000000c47c, HASH[8c891440a3fdef244cefe3a928529f037fed7f6347509fc814f82430366f39df], 15))
25: Parent
26: Push(HashWithSum(kv_hash=HASH[b81a70230e6c91bdbcb631bed29cbb79815f3117da15b29f771a7f833d42090c], left=HASH[24c61905d7361140b4ca79506c2892a081daab4b471171b216f59968fd8855b5], right=HASH[8f4b5b0b1135c8b4c87597f33a968b8404bda5d2d88ee7d2b613d90d1795fb4a], sum=7))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVDigestSum(0x800000000000c79c, HASH[387d1806a39adf033f4e7a7888970a627bfd803850eee18e8a277f45893f5e61], 1000))
34: Parent
35: Push(HashWithSum(kv_hash=HASH[a7d78a4571fc667589d9194ff86f9ebb7dc5c7b1c86241ee0fd1231fa877e47e], left=HASH[1bd878de2c7e1752d4e5521d36a8d3ab2854f652d6cf4be41cb1cd48cbb2cfe3], right=HASH[42073ef99149a312e57e2c165578c2ed4f62e9325f400102da26d3077916ca21], sum=488))
36: Child)
}
}
}
}
}
// ↓↓↓ Q9 abbreviation ↓↓↓
// 99 more recipient buckets follow here
// (recipient_001 .. recipient_099), each a
// LayerProof with the same structural shape as
// recipient_000 above: a single-key continuation
// Tree → NotSummed(ProvableSumTree) → AggregateSumOnRange
// collapse. Per-bucket aggregate sums follow
// recipient_n.sum = 500 × ((n % 10) + 1)
// with recipient_000.sum = 499 (the boundary
// row 50000 is excluded by the half-open `>` range).
// ↑↑↑ Q9 abbreviation ↑↑↑
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. Q9 is the largest proof in the chapter and structurally distinct from every other query: the AST tree fans out at layer 6 (the byRecipient subtree) where the carrier outer walk reveals all 100 In-key terminators inline (each as a KVValueHash(recipient_NNN, SumTree(73656e744174, sum_NNN)) op), and each of those 100 keys' lower_layers entry then carries its own inner AggregateSumOnRange descent through that recipient's byRecipientTime continuation — a per-bucket Q8-shaped boundary walk producing one aggregate i64 per recipient. The total LayerProof count is 5 fixed (root → @ → contract → 0x01 → tip) + 1 outer byRecipient layer that lists all 100 In keys + 100 × 2 per-bucket layers (one for the recipient-continuation single-key tree, one for the byRecipientTime sentAt range collapse). The verifier (via GroveDb::verify_aggregate_sum_query_per_key) walks each branch, collapses its inner range to a single sum, and returns Vec<(in_key, i64)> with 100 entries:
(recipient_000, 499) # 499 tips: rows 50100, 50200, …, 99900, each amount=1
(recipient_001, 1000) # 500 × 2
(recipient_002, 1500) # 500 × 3
…
(recipient_009, 5000) # 500 × 10
(recipient_010, 500) # 500 × 1 (cycle restarts)
…
(recipient_099, 5000) # 500 × 10
Note recipient_000 gets 499 not 500: sentAt > 50000 excludes the boundary row (sentAt = 50000) which belongs to recipient_000 (row 50000 → recipient_(50000 % 100) = recipient_0), so its 500-row window loses one entry. Every other recipient is unaffected.
Verified root hash 95aa7470…71b6.
Diagram: per-layer merk-tree structure
Q9's structure is the chapter's only non-linear descent: instead of a single chain, layer 7 fans out into 100 parallel branches (one per resolved In-key), each running its own inner-range collapse. The diagram below shows one branch in detail and indicates the × 100 fan-out at the outer layer.
flowchart TB
subgraph L6["Layer 6 — byRecipient merk-tree (CARRIER OUTER LAYER)"]
direction TB
L6_targets["100 KVValueHash outer-key terminators<br/>(recipient_000 … recipient_099)<br/>each value=<b>SumTree(73656e744174, per-recipient-sum)</b><br/>but here the values are mid-descent pointers, NOT the<br/>carrier-aggregate's final output — each one's<br/>lower_layers fans out to a per-bucket range walk."]:::queried
L6_boundary["~22 merk-boundary ops shared across all 100 descents:<br/>since every byRecipient entry is revealed,<br/>only the merk-root spine stays opaque"]:::sibling
L6_targets --> L6_boundary
end
subgraph L7a["Layer 7 — recipient_NNN continuation (× 100 parallel, one per bucket)"]
direction TB
L7a_q["<b>sentAt</b><br/>kv_hash=HASH[…]<br/>value: NotSummed(ProvableSumTree(…, 1000))<br/>(per-recipient continuation, single-key tree)"]:::queried
L7a_sib["1 Hash opaque-sibling commit<br/>(per the per-bucket continuation merk-tree)"]:::sibling
L7a_q --> L7a_sib
end
subgraph L8a["Layer 8 — byRecipientTime sentAt range collapse (× 100 parallel, one per bucket)"]
direction TB
L8a_collapse["<b>AggregateSumOnRange(sentAt > 50000)</b><br/>verified sum_NNN per recipient_NNN<br/>(499 for recipient_000;<br/>500 × ((NNN % 10) + 1) for the rest)"]:::target
L8a_boundary["Range-boundary commitments (~36 ops per bucket):<br/>same HashWithSum + KVDigestSum shape as Q8's layer 8.<br/>This per-bucket cost × 100 buckets is what dominates Q9's<br/>169 064 B vs the non-carrier shapes (Q5 12 064 B, Q6 9 784 B)."]:::pst
L8a_collapse --> L8a_boundary
end
L6_targets -. "100 fan-out: each value=SumTree(merk_root[per-bucket]) → L7a" .-> L7a_q
L7a_q -. "value=NotSummed(ProvableSumTree(merk_root[byRT.sentAt])) → L8a" .-> L8a_collapse
fanout["× 100 parallel descents<br/>(one per resolved In key)"]:::note
L7a_q -.-> fanout
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef pst fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
classDef note fill:#21262d,color:#c9d1d9,stroke:#fb8500,stroke-width:2px,stroke-dasharray: 6 4;
This is the structural payoff of carrier composition: instead of 100 independent Q8-shaped proofs each running its own full root→@→contract→…→ recipient_NNN → sentAt descent (with the upper 5 fixed layers re-paid for each branch), Q9 amortizes the upper-5 layers across all 100 buckets and only pays the per-bucket cost (1 continuation layer + 1 range-collapse layer ≈ 1 690 bytes) per outer key. The 64% byte savings vs the N-independent baseline come almost entirely from that shared prefix amortization.
The carrier composition is the only way to get per-bucket range sums in a single proof. The alternative — issuing N independent AggregateSumOnRange proofs, one per In value — would take N × 2 657 bytes ≈ 265 700 bytes for k=100, plus N round-trips. Carrier collapses that to one proof, one round-trip, and ≈ 64% of the byte cost.
The two carrier-aggregate gates worth knowing:
SizedQuery::limitcaps the outer walk (herelimit = 100accommodates all distinct recipients; for an open-ended outerRangeclause it bounds how many In-branches the proof commits). The verifier rebuilds the samelimitbyte-for-byte; mismatched limits break the merk-root recomputation.SizedQuery::offsetis rejected for carrier-aggregate (would change which(outer_key, sum)pairs end up in the proof; the use case isn't designed yet). Mirrors the count-side carrier-ACOR contract.
The PCPS variant — AggregateCountAndSumOnRange on a carrier — exists too; same primitive, but returns (outer_key, u64 count, i64 sum) triples for indexes that opt into both rangeCountable: true and rangeSummable: true. Drive-side support is wired (DriveDocumentSumQuery::verify_carrier_aggregate_count_and_sum_proof); a worked PCPS-carrier example will live in a separate combined-feature chapter alongside its own contract.
Range Modes — Distinct Variant
Every range query above runs in aggregate mode (return_distinct_sums_in_range = false, the default). Setting it to true returns one SumEntry per distinct property value in the range — the histogram shape.
For Query 7 with return_distinct_sums_in_range = true:
SumEntry { in_key: None, key: serialize_value_for_key("sentAt", 50001), sum: 2 }
SumEntry { in_key: None, key: serialize_value_for_key("sentAt", 50002), sum: 3 }
...
SumEntry { in_key: None, key: serialize_value_for_key("sentAt", 99999), sum: 10 }
49 999 entries (one per distinct sentAt in the range). The prove path's proof size in this mode is O(distinct values matched), not O(log n) — for tip-jar's per-row-distinct sentAt fixture that's a big difference. Pick the aggregate mode when you want one number, distinct when you want a histogram. Same trade-off as count's Range Queries on the Prove Path.
References With Sum Item — Where They Live
Every non-primary-key sum lookup above lands on a value-tree path like:
[..., "<index_property>", "<index_value>", 0]
where [0] (key zero) holds a SumTree of references — one per document — and the parent value-tree's sum is the aggregation of those references' sum-item contributions. The reference primitive grovedb exposes is Element::ReferenceWithSumItem(reference_path, sum_value, flags) (grovedb PR 670): a single element that dereferences to the document body in primary storage and carries an i64 (the amount value at insert time). When merk computes node hashes up the parent SumTree, the reference's sum-item is what propagates.
Two element types for two roles, kept distinct:
- Primary storage at
[doctype, 0, doc_id]usesElement::ItemWithSumItem(serialized_doc, sum_value, flags)when the doctype declaresdocumentsSummable: "amount"— the document body lives there inline AND contributes to the primary-key SumTree's running aggregate. This is what makes thedocumentsSummablefast path (O(log n) total-sum proof, no index needed) actually work: the primary-key SumTree's root carries the real sum because every leaf is sum-bearing. - Index references at
[index_path, value, 0, doc_id]useElement::ReferenceWithSumItem— a true reference (so document iteration via index walks still dereferences to the body in primary storage, exactly likeElement::Referencedoes on the count side) AND a sum contribution that propagates to ancestor sum trees.
That's the load-bearing primitive for the design: without it, every non-primary-key sum index would need to either duplicate the document under the index (storage blowup) or refuse to participate in sum aggregation (no non-primary-key sums at all). With it, the existing index storage shape extends from "reference under [0]" to "reference with sum-item under [0]" — same descent, same proof verification, plus the running-sum propagation.
The reference's stored sum-item is fixed at insert time. On delete, Drive reads it back out of the reference (rather than re-reading the source document) and subtracts it from every ancestor sum tree. That's why the named summable property must be in required: a missing amount at insert time would leave the reference with no sum contribution, and a later delete would underflow the ancestor sums by zero — a quiet corruption that's never worth allowing.
At-a-Glance Comparison
| Query | Index used | Element shape at terminator | Returned variant | Proof primitive |
|---|---|---|---|---|
| 1 — Total | (doctype primary-key) | SumTree at tip/[0] | aggregate_sum | merk path |
| 2 — Equal on byRecipient | byRecipient | SumTree at recipient/recipient_050 | aggregate_sum | merk path |
| 3 — Equal on bySentAt | bySentAt | SumTree at sentAt/serialize(50000) | aggregate_sum | merk path |
| 4 — Compound Equal | byRecipientTime | SumTree at recipient/recipient_050/sentAt/serialize(50000) | aggregate_sum | merk path |
| 5 — In on byRecipient | byRecipient | k × SumTrees | entries | k × merk path |
| 6 — In on bySentAt | bySentAt | k × SumTrees | entries | k × merk path |
| 7 — Range on bySentAt | bySentAt | (collapsed boundary) | aggregate_sum | AggregateSumOnRange |
| 8 — Compound + Range | byRecipientTime | (collapsed boundary, prefix descent) | aggregate_sum | AggregateSumOnRange |
| 9 — Carrier (In + Range) | byRecipientTime | k × (collapsed boundaries under outer Keys) | per-key entries | verify_aggregate_sum_query_per_key (carrier composition) |
The split is structurally the same as count's — single value-tree lookups for point queries (1–6), AggregateSumOnRange for range (7–8), and carrier composition for per-bucket range sums (9) — with SumTree/ProvableSumTree and aggregate_sum/SumEntry slotted in where count had CountTree/ProvableCountTree and aggregate_count/CountEntry. The Q9 carrier specifically wraps the leaf AggregateSumOnRange primitive once per outer In branch and stitches the per-bucket sums into a single verifiable response — saving N round-trips and ~36% of the bytes versus issuing N independent range-sum proofs.
What's Next
The full SUM surface is wired end-to-end: primary-key total (Query 1), point-lookup, In-fan-out, AggregateSumOnRange on both top-level and compound indexes, and the carrier-aggregate primitive. All nine queries verify against the shared root hash 95aa7470…71b6.
A Sum Index Group By Examples sibling chapter is queued mirroring the count GROUP BY chapter — the byRecipient index supports both per-recipient and group-by-recipient semantics, and the bench's report_group_by_matrix already publishes the full matrix (visible inline in bench -- --test output under [matrix]).