diff --git a/Cargo.lock b/Cargo.lock index 6fabfe1fe26..9ea5c271632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,7 +576,7 @@ dependencies = [ "bitflags 2.13.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "proc-macro2", "quote", "regex", @@ -1229,7 +1229,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2474,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2954,7 +2954,7 @@ dependencies = [ [[package]] name = "grovedb" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "axum 0.8.9", "bincode", @@ -2992,7 +2992,7 @@ dependencies = [ [[package]] name = "grovedb-bulk-append-tree" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "bincode", "blake3", @@ -3008,7 +3008,7 @@ dependencies = [ [[package]] name = "grovedb-commitment-tree" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "blake3", "grovedb-bulk-append-tree", @@ -3024,7 +3024,7 @@ dependencies = [ [[package]] name = "grovedb-costs" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "integer-encoding", "intmap", @@ -3034,7 +3034,7 @@ dependencies = [ [[package]] name = "grovedb-dense-fixed-sized-merkle-tree" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "bincode", "blake3", @@ -3047,7 +3047,7 @@ dependencies = [ [[package]] name = "grovedb-element" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "bincode", "bincode_derive", @@ -3062,7 +3062,7 @@ dependencies = [ [[package]] name = "grovedb-epoch-based-storage-flags" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "grovedb-costs", "hex", @@ -3074,7 +3074,7 @@ dependencies = [ [[package]] name = "grovedb-merk" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "bincode", "bincode_derive", @@ -3100,7 +3100,7 @@ dependencies = [ [[package]] name = "grovedb-merkle-mountain-range" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "bincode", "blake3", @@ -3111,7 +3111,7 @@ dependencies = [ [[package]] name = "grovedb-path" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "hex", ] @@ -3119,7 +3119,7 @@ dependencies = [ [[package]] name = "grovedb-query" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "bincode", "byteorder", @@ -3135,7 +3135,7 @@ dependencies = [ [[package]] name = "grovedb-storage" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "blake3", "grovedb-costs", @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "grovedb-version" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "thiserror 2.0.18", "versioned-feature-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3163,7 +3163,7 @@ dependencies = [ [[package]] name = "grovedb-visualize" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "hex", "itertools 0.14.0", @@ -3172,7 +3172,7 @@ dependencies = [ [[package]] name = "grovedbg-types" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?tag=v5.0.1#6bb2ebff62a842a3c416126bbcfcf7774dfe0262" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" dependencies = [ "serde", "serde_with 3.21.0", @@ -3588,7 +3588,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "system-configuration", "tokio", "tower-service", @@ -3839,7 +3839,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4651,7 +4651,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5539,7 +5539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck 0.4.1", - "itertools 0.14.0", + "itertools 0.10.5", "log", "multimap", "petgraph", @@ -5560,7 +5560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -5573,7 +5573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -5709,7 +5709,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.5.10", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -5747,7 +5747,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.4", "tracing", "windows-sys 0.59.0", ] @@ -6569,7 +6569,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6628,7 +6628,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7488,7 +7488,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8937,7 +8937,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0298cdaf3f6..28c9c108f91 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -66,6 +66,8 @@ - [Document Sum Trees](drive/document-sum-trees.md) - [Sum Index Examples](drive/sum-index-examples.md) - [Average Index Examples](drive/average-index-examples.md) +- [Document Ranked Trees](drive/document-ranked-trees.md) +- [Ranked Index Examples](drive/ranked-index-examples.md) # Testing diff --git a/book/src/drive/document-ranked-trees.md b/book/src/drive/document-ranked-trees.md new file mode 100644 index 00000000000..07db94bad79 --- /dev/null +++ b/book/src/drive/document-ranked-trees.md @@ -0,0 +1,364 @@ +# Document Ranked Trees + +The aggregate surfaces that landed in Platform 4.0 answer *"how many"* and *"how much"* — per document type, per indexed value, per range of indexed values. What none of them answer is *"which groups score highest"*. Counting the reviews of every restaurant is O(log n) per restaurant; finding the **five best-rated restaurants** meant enumerating all of them and sorting client-side, with a proof that grew with the number of restaurants rather than with the number you asked for. + +From protocol v14 an index can declare that its groups are **rankable** by an aggregate. The terminal tree upgrades to grovedb's *indexed-tree* family (grovedb PR #657), which carries an ordered secondary Merk per declared ranking axis, and "top 5 restaurants by average grade" becomes an **O(log n + k)** read with an O(log n + k) proof. This chapter explains the three indexed tree variants, how an index opts into one, how the secondaries are keyed and maintained, and how the feature composes with the other v14 change. The [Ranked Index Examples](./ranked-index-examples.md) chapter is the worked-example companion. + +The chapter assumes you've read [Document Count Trees](./document-count-trees.md), [Document Sum Trees](./document-sum-trees.md), and the [Average Index Examples](./average-index-examples.md) chapter. Ranked trees are built directly on top of the range-aggregate layouts those chapters describe: a ranking secondary is an *ordering* over aggregates the range layout already maintains, so the range axis is a hard prerequisite for the ranked one. + +> **Status:** implemented and gated at protocol version 14. The storage layout is pinned end-to-end against a real grovedb by [`ranked_index_e2e_tests`](https://github.com/dashpay/platform/blob/v4.2-dev/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs), which runs against the restaurants fixture at [`packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json`](https://github.com/dashpay/platform/blob/v4.2-dev/packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json) — the same fixture the examples chapter walks through. The grovedb-side design lives in that project's book chapter **The CountIndexedTree** (`docs/book/src/count-indexed-tree.md`), which is the authoritative reference for the element layout, the hash composition, and the secondary-Merk storage prefixes summarised here. + +## Why Ranking Needs a New Primitive + +A `ProvableCountTree` at the property-name level stores, per group, the group's document count — and stores it in a Merk keyed by the **group key**. That is exactly what you want for "how many reviews does restaurant `alpha` have?" and for "how many reviews do restaurants between `alpha` and `mike` have?": both questions are answered by walking a key-ordered boundary. + +"Which five restaurants have the most reviews?" is a question about the *aggregate*, not about the key. Nothing in the key-ordered Merk correlates position with count, so the only honest answer is to visit every group. That is O(n) work and — worse — an O(n) proof, even though the answer is five entries. Three options exist: + +1. **Enumerate and sort client-side.** Correct, and the only thing 4.0 could do. Proof size scales with the number of groups, not with `k`; a contract with 50 000 restaurants pays 50 000 committed entries to learn about five. +2. **Have the server sort and return the top five.** O(1) bytes, zero cryptographic value — the client has no way to check that the sixth-best restaurant wasn't quietly omitted. +3. **Maintain a second, aggregate-ordered view of the same groups, committed to the same root hash.** Top-k becomes a bounded range read at one end of that view, and its proof is a standard Merk range proof over `k` entries. **This is what this chapter is about.** + +The grovedb primitive is the **indexed tree**: a primary Merk that is a byte-compatible mirror of the tree it replaces, plus one ordered **secondary** Merk per declared axis, keyed so that the aggregate sorts first and the group key only breaks ties. Because the primary is byte-compatible, every pre-existing range-aggregate query against the same tree — `AggregateCountOnRange`, `AggregateSumOnRange`, `AggregateCountAndSumOnRange` — keeps working unchanged against an index that adds a ranking axis. + +*The dashed box is the wrapping `Element`. The primary Merk (blue) is keyed by group key and is byte-identical to the `ProvableCountTree` it replaces. The secondary (orange) holds one entry per group, keyed by `count_be ‖ group_key`, so a descending walk of its right edge yields the highest-count groups first.* + +```mermaid +flowchart LR + subgraph PCIT ["ProvableCountIndexedTree"] + direction TB + subgraph ELEM ["Tree element c=9"] + direction TB + P["primary root
(keyed by group key)"]:::primary + S["secondary root
(keyed by count ‖ group key)"]:::secondary + end + end + + P --> PA["alpha c=2"]:::leaf + P --> PM["mike c=3"]:::leaf + P --> PZ["zulu c=4"]:::leaf + + S --> SA["0x…02 ‖ alpha"]:::leaf + S --> SM["0x…03 ‖ mike"]:::leaf + S --> SZ["0x…04 ‖ zulu"]:::leaf + + classDef primary fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px; + classDef secondary fill:#fb8500,color:#0d1117,stroke:#fb8500,stroke-width:2px; + classDef leaf fill:#21262d,color:#c9d1d9,stroke:#484f58; + + style ELEM fill:none,stroke:#1f6feb,stroke-width:2px,stroke-dasharray: 6 4,color:#1f6feb +``` + +`ORDER BY … DESC LIMIT 2` reads the two right-most secondary entries (`zulu`, `mike`) and proves them with a standard Merk range proof — three levels of hashes, two committed entries. The same question against the primary alone would have to commit all nine groups. + +## Contract Grammar + +Ranking is an **index-level** opt-in. Three independent keywords, one per axis: + +| Keyword | Ranks groups by | Requires (in effect) | Rust field | +|---|---|---|---| +| `rankedCountable` | each group's document count | `rangeCountable: true` | `Index::ranked_countable` | +| `rankedSummable` | each group's sum of the `summable` property | `rangeSummable: true` | `Index::ranked_summable` | +| `rankedAverageable` | each group's average of the `averageable` property | `rangeAverageable` semantics — **both** `rangeCountable` and `rangeSummable` | `Index::ranked_averageable` | + +The three axes are **independent opt-ins**. `rankedAverageable` is *not* sugar for the other two, unlike `averageable` / `rangeAverageable`, which genuinely are sugar for their count+sum longhand. Each ranking axis costs its own ordered secondary Merk and its own maintenance on every write, so each is declared explicitly. Declaring `rankedAverageable` alone is a legal — and usually the right — choice. + +The range prerequisite is a hard requirement, not a convenience: a ranking secondary orders the *per-group aggregates the range layout already maintains*. Without the range axis the terminal property-name tree carries no per-group aggregate to sort by. The parser rejects the combination with a message naming the missing flag: + +```text +rankedCountable requires rangeCountable: true; ranking groups by count needs +the per-group counts the range-count layout maintains +``` + +### Value-Sensitive Prerequisites in the Meta-Schema + +The document meta-schema enforces the same prerequisites, but it cannot use the `dependentRequired` rows the `range*` keywords use. `dependentRequired` fires on **key presence**, so a written-out opt-out — `"rankedCountable": false`, which the structural parser accepts as exactly that — would be made to demand a `rangeCountable` the index does not need. Meta-schema v3 therefore expresses the ranked prerequisites as value-sensitive `if` / `then` pairs: + +```json +{ + "if": { + "properties": { "rankedCountable": { "const": true } }, + "required": ["rankedCountable"] + }, + "then": { "required": ["rangeCountable"] } +} +``` + +The `range*` rows keep their presence semantics because that is what they shipped with in v2, and changing them would move historical validation results. + +One asymmetry is worth knowing when authoring: **the meta-schema demands the literal key, the parser accepts the effect.** `rankedAverageable: true` needs a literal `rangeAverageable: true` to satisfy the schema's `then`, even though the parser is satisfied by the explicit `countable` + `summable` + `rangeCountable` + `rangeSummable` longhand. Since full JSON-schema validation only runs under `full_validation`, both layers matter — write the sugar form and the two agree. + +### Shape Restrictions + +Two structural rules, both enforced at contract-parse time in rs-dpp: + +- **Single-property indexes only.** `ranked aggregates are only supported on single-property indexes in this protocol version`. Two reasons, both relaxable at a future protocol version. First, a compound index whose *prefix* level also terminates an aggregating index would need its ranked terminal tree wrapped in a `NonCounted` / `NotSummed` shell so it contributes zero to the parent's aggregate — and the storage layer structurally rejects any wrapper around an indexed tree, because the wrapper would neutralise the very aggregates the secondaries order by. (Drive's fail-closed guard for this is `INDEXED_INNER_UNWRAPPABLE`.) Second, the ranked query surface has no equality-prefix routing: with more than one property there would be a prefix to fix before ranking, and nothing to express it with. +- **Non-unique indexes only.** `ranked aggregates are not supported on unique indexes: each group of a unique index contains at most one document, so there is nothing meaningful to rank`. Contested indexes are covered transitively — a contested index is unique by construction, so it hits the same check rather than needing its own. + +### Version Gate + +The grammar activates with **protocol version 14** through two independent gates that must agree: + +- `CONTRACT_VERSIONS_V6` points `document_type_schema` at the **v3 document meta-schema**, which hosts the three keywords. v13 keeps validating against v2, where they fail an index entry's `additionalProperties: false`. +- `try_from_schema: 3` selects **parser generation 3**, which is the only generation that passes `ranked_aggregates_allowed = true` into `Index::try_from_value_map`. With that flag off, the three keywords are not part of the grammar at all: they fall through to the unknown-key arm and are rejected with exactly the error a pre-v14 node produced for them. + +The doubled gate is load-bearing. The meta-schema only runs under `full_validation`, so a non-validating parse — `check_tx`, contract-cache warm-up, state restore — could otherwise smuggle a ranked index past a node whose protocol version has no idea how to lay one out on disk. + +**On the parser-generation pattern.** Generation 3 is a *full copy* of generation 2 (and of the generation-1 core that wrapper delegates to), not a version gate threaded into those modules. That is the repository's standing rule for grammar introduced by a new protocol version: shipped generations stay byte-identical to the code consensus already ran, so replaying a historical block can never pick up grammar that did not exist when the block was produced. The copy is kept structurally line-for-line with its sources, so a diff against `v1/mod.rs` + `v2/mod.rs` shows only the ranked deltas. + +## How Drive Picks the Indexed Tree Variant + +An index's **terminal property-name tree** is the level whose children are the last index property's value trees — one child per distinct value, i.e. one child per *group*. Without ranking flags its `TreeType` comes purely from the range flags: + +| `range_countable` | `range_summable` | Base tree type | +|---|---|---| +| `true` | `true` | `ProvableCountProvableSumTree` | +| `true` | `false` | `ProvableCountTree` | +| `false` | `true` | `ProvableSumTree` | +| `false` | `false` | `NormalTree` | + +A ranking flag upgrades that base to its indexed mirror: + +| Base | Declared axes | Indexed tree | +|---|---|---| +| any | `[]` | unchanged (no ranking declared) | +| `ProvableCountTree` | `[Count]` | `ProvableCountIndexedTree` | +| `ProvableSumTree` | `[Sum]` | `ProvableSumIndexedTree` | +| `ProvableCountProvableSumTree` | any non-empty | `ProvableCountProvableSumIndexedTree(axes)` | + +The single-axis variants (PCIT / PSIT) carry **no axis list on the element at all** — their one secondary is implied by the variant — which is why they are only reachable when the base layout is already single-axis. An index that declares, say, `rankedCountable` alongside `rangeSummable` still lays out as `ProvableCountProvableSumTree` underneath, so it upgrades to the multi-axis **PCPSIT** carrying just the Count axis in its TLV. + +The axes list is canonical: sorted by tag (`0 = Count`, `1 = Sum`, `2 = Avg`), deduped, one to three entries. Freshly created, the element's TLV is `[(tag, None), …]` — every secondary starts empty. grovedb validates the list on construction (`Element::validate_pcpsit_axes`), because an out-of-order, duplicated or empty TLV would still be hashed into the parent and produce a tree whose secondaries nothing can address. + +Selection lives in [`packages/rs-drive/src/drive/document/ranked_index_tree_type.rs`](https://github.com/dashpay/platform/blob/v4.2-dev/packages/rs-drive/src/drive/document/ranked_index_tree_type.rs): + +```rust +pub(crate) fn ranked_property_name_tree_type( + base: TreeType, + ranked_axes: &[IndexAxis], +) -> Result { + if ranked_axes.is_empty() { + return Ok(base); + } + match (base, ranked_axes) { + (TreeType::ProvableCountTree, [IndexAxis::Count]) => Ok(TreeType::ProvableCountIndexedTree), + (TreeType::ProvableSumTree, [IndexAxis::Sum]) => Ok(TreeType::ProvableSumIndexedTree), + (TreeType::ProvableCountProvableSumTree, _) => { + Ok(TreeType::ProvableCountProvableSumIndexedTree) + } + _ => Err(Error::Drive(DriveError::CorruptedContractIndexes(/* … */))), + } +} +``` + +The catch-all arm is unreachable given rs-dpp's parse-time invariants (`ranked_countable ⇒ range_countable`, `ranked_summable ⇒ range_summable`, `ranked_averageable ⇒ both`), which force `base` to be provable on every axis a ranking flag names. It is a typed error rather than a silent fallback so a future grammar change that breaks one of those implications surfaces here instead of quietly laying down a tree whose secondaries nothing maintains. + +`property_name_tree_type_and_ranked_axes()` is the single source of truth — contract registration, contract update, the document insert / update / delete index walkers, and the cost-estimation layers all route through it, so the layouts they describe cannot drift apart. The contract insert/update paths reach grovedb through three helpers parallel to the count and sum families: + +- `batch_insert_empty_provable_count_indexed_tree` — PCIT. +- `batch_insert_empty_provable_sum_indexed_tree` — PSIT. +- `batch_insert_empty_provable_count_provable_sum_indexed_tree` — PCPSIT, taking the axes TLV. + +**The groups keep their ordinary value-tree types.** An indexed primary is a mirror of the tree it replaces, so nothing changes one level down: with the restaurants fixture, `review`'s groups are `ProvableCountProvableSumTree`s, `visit`'s are `CountTree`s, and `tip`'s are `SumTree`s — exactly the shapes the range flags alone would have produced. + +## The Secondary Merks + +Each declared axis gets one ordered secondary Merk holding **one entry per group**, keyed by + +```text +axis_sort_key ‖ group_key +``` + +so the aggregate sorts first and the group key breaks ties. Ties therefore come back in group-key order *in the direction of the walk* — descending group-key order for `TOP`, ascending for `BOTTOM`. + +| Axis | Sort key | Width | Encoding | +|---|---|---|---| +| Count | `count` | 8 B | big-endian `u64` | +| Sum | `sum` | 8 B | big-endian `i64` with the sign bit flipped | +| Avg | `floor(sum × SCALE / count)` | 16 B | big-endian `i128` with the sign bit flipped | + +Sign-bit toggling is what makes the signed encoders order-preserving: flipping bit 63 of an `i64` (or bit 127 of an `i128`) maps the two's-complement signed range onto an unsigned range with the same ordering, so plain byte comparison sorts negatives below positives. + +### The Avg Fixed-Point Sort Key + +There is no fractional key type, so the Avg axis sorts by a fixed-point integer: + +```rust +pub fn compute_avg_fixed_point(sum: i64, count: u64) -> i128 { + if count == 0 { + return 0; + } + (sum as i128) + .saturating_mul(AVG_FIXED_POINT_SCALE) + .div_euclid(count as i128) +} +``` + +Three properties to internalise: + +- **The scale is grovedb's constant, not a wire constant.** `AVG_FIXED_POINT_SCALE` is `10^19` today; it moved from `10^15` before release. Drive re-exports it (`drive::query::RANKED_AVG_SCALE`, itself re-exported by `drive_proof_verifier::RANKED_AVG_SCALE`) rather than re-declaring it, so the two can never drift — the encoded sort keys in storage are produced with grovedb's constant, and a platform-side copy that fell out of step would silently mis-scale every average a client renders. **Never hardcode the literal.** +- **Division is euclidean, i.e. floor toward −∞.** Rust's `/` truncates signed integers toward zero, which would place negative averages one fixed-point bucket too high. The restaurants fixture carries a whole document type (`adjustment`, whose `delta` admits negative values) for the sole purpose of exercising signed sums and this rounding mode. +- **`0 / 0` is defined as `0`.** An empty group has no entry in the secondary at all, so this is a defensive definition rather than an observable one. + +### Maintenance + +Secondaries are maintained through the **normal batch write path** — there is no separate reindex step and no background job. Every document insert, update and delete that changes a group's `(count, sum)` also rewrites that group's entry in each declared secondary: the old `(sort_key ‖ group_key)` entry is removed and the new one inserted, in the same grovedb batch, under the same block. Draining a group's last document removes the group from the primary and its entry from every secondary together. + +grovedb's own consistency verifier enforces the invariant directly: every primary entry at `key` must have exactly one secondary entry at `make_axis_secondary_key(axis, count, sum, key)`, and the secondary must contain nothing else. + +## Hash Composition + +An indexed tree commits **two Merks in one element**. The composition grovedb uses — internally called **H1-A** — is a single three-input Blake3 call: + +```text +combined_value_hash = combine_hash_three(value_hash, primary_root_hash, axes_digest) +``` + +For the single-axis variants the third input is simply that axis's root hash. For PCPSIT it is `axes_digest`, a length-prefixed Blake3 over the canonical axes TLV — `[n]` followed by `(tag, root_hash)` per axis. The length prefix is what distinguishes a one-axis digest from a two-axis digest truncated to a single entry. An axis with no entries yet contributes `NULL_HASH` in its slot. + +The three-input form is deliberate and regression-tested: it must **not** be equivalent to `combine_hash(a, combine_hash(b, c))`, the nested composition rejected during design review because it would have doubled the hash work per bubble-up. + +What this buys the verifier: the parent's `value_hash` binds the primary root *and* every secondary root. A proof of a top-k walk over one secondary reconstructs that secondary's root, recombines it with the primary root the same way the writer did, and the result has to match what the parent committed. A server cannot serve a stale or forged secondary while presenting an honest primary. + +## The Grove Path + +Every ranked read — and, on the prove path, every ranked proof — is issued against the path of the **terminal property-name tree**. For a single-property index that is: + +```text +[ RootTree::DataContractDocuments as u8 ] // 0x01 + / + / [ 0x01 ] // "documents", not "contract" + / // e.g. b"review" + / // e.g. b"restaurantId" +``` + +The children of that tree are the *groups*: one value tree per distinct value of the last index property, keyed by the raw index-key bytes of that value (for a `string` property, its UTF-8 bytes — e.g. `b"alpha"`). The secondary entries a top-k read returns are keyed by those same group keys. A compound index `[a, b]` inserts ` / ` between the doctype and the terminal `` level — which is exactly the shape ranked indexes don't support yet. + +Prover and verifier build this path through the same function, `DriveDocumentRankedQuery::indexed_property_name_tree_path`, which is why they agree on the root hash by construction. + +## Write-Path Cost: The Grove v4 Cleanup Gates + +Indexed trees need two batch behaviours that earlier grove versions don't have, so protocol v14 also moves Drive from `GROVE_V3` to **`GROVE_V4`**: + +- **`delete_tree_cleanup_type_source`** — a batch `DeleteTree` reads the stored element and uses its **actual** type to select cleanup namespaces, rejecting a declared/stored mismatch that involves an indexed tree. V1–V3 take the declared type at face value. +- **`overwrite_indexed_cleanup_inspection`** — a batch overwrite of a non-reference element (with tree-override protection off) reads the stored element to detect an indexed tree being overwritten, scheduling its per-axis secondary storage for cleanup or refusing the ambiguous case. + +Without them, a batch overwrite of a ranked index would orphan its per-axis secondary storage. Indexed trees only exist from protocol v14, so activating the stricter cleanup alongside them costs older versions nothing. + +Both gates are **cost-neutral**: they derive the old element from data the merk apply already loads when it rewrites or deletes a key, so no extra stored-element read is charged and v14's fee constants match v13's. Three facts pin this: + +- **Fees are identical across the boundary.** The identity-balance, token-balance and state-transition processing-fee pins carry the same values at protocol v13 and v14, and `run_chain_one_identity_in_solitude` has a protocol-version-13 sibling asserting the identical end balance — the pair proves the v13 → v14 transition changes nothing about those runs' fees. +- **Storage fees are untouched everywhere** — the gates only observe, never write. +- **Cleanup still happens.** A batch overwrite of an indexed tree schedules its per-axis secondary storage for cleanup (or refuses the ambiguous case), and `DeleteTree` uses the actual stored type — covered by grovedb's own overwrite and delete-tree suites at the pinned revision. + +## Interaction With the Shared-Prefix Aggregate Fix + +Protocol v14 hosts two consensus changes, and contracts exist where both apply to the same index. The [shared-prefix aggregate fix](https://github.com/dashpay/platform/blob/v4.2-dev/packages/rs-drive/src/drive/document/index_level_tree_types.rs) makes a contract legal that previously registered but rejected every document insert: an aggregating index terminating at a property that is also the *prefix* of a compound index (e.g. `summable [a]` next to `[a, b]`). + +The two changes are **orthogonal by construction**, and they live exactly one level apart: + +- The **ranked upgrade** decides the *property-name* tree type — plain → indexed mirror. +- The **continuation demotion** decides the *value* tree type one level below it — a provable count-bearing value tree that has to host a compound continuation demotes to `CountSumTree`, since grovedb rejects count-suppressed children under provable count parents by design. + +A demoted `CountSumTree` value tree contributes its `(count, sum)` to a ranked indexed parent exactly as the provable variant did, so a ranked index's secondaries keep ranking correctly over shared-prefix shapes. Concretely, for a `dish` doctype with a ranked `[restaurantId]` index alongside a plain compound `[restaurantId, chefId]`: + +- the property-name tree at `restaurantId` stays a `ProvableCountProvableSumIndexedTree` carrying the Avg axis; +- each group's value tree demotes from `ProvableCountProvableSumTree` to `CountSumTree`; +- the `chefId` continuation inside it goes in `Element::NonCounted`, contributing zero to the group's count and sum. + +The one place the two changes genuinely collide is the case the single-property rule already forbids: a ranked *terminal* level sitting inside an aggregating value tree would need a wrapper, and an indexed tree can never be wrapped. That is the `INDEXED_INNER_UNWRAPPABLE` guard, and it fails closed. + +## Storage-Layout Invariants + +All three ranking flags are **immutable** across a contract update, for the same reason and with the same error as the count and sum flags. The set of declared axes picks the indexed tree variant and its ordered secondaries at contract creation; toggling any one of them would require rebuilding the secondaries for every existing group. + +`IndexLevel::find_first_ranked_change` walks the two index-level trees and returns the first path where `ranked_countable`, `ranked_summable` or `ranked_averageable` differs — e.g. `restaurantId -> (ranked_averageable: false -> true)`. `IndexLevel::validate_update` turns that into a `DataContractInvalidIndexDefinitionUpdateError`: + +```text +Document with type {document_type} could not add or remove '{index_path}' during +data contract update as we do not allow modifications of data contract index paths +``` + +Adding a *new* ranked index on update is rejected by the same machinery. Don't relax these guards: an index whose declared axes disagree with the element on disk would have the write path maintaining secondaries the reader can't address, or the reader reading secondaries the write path never updates — consensus drift either way. + +## Authoring a Contract That Uses Ranked Trees + +The restaurants fixture is the reference. Its `review` doctype ranks restaurants by average grade: + +```json +{ + "review": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurant", + "properties": [{ "restaurantId": "asc" }], + "countable": "countable", + "summable": "grade", + "averageable": "grade", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true + } + ], + "properties": { + "restaurantId": { "type": "string", "minLength": 1, "maxLength": 32, "position": 0 }, + "grade": { "type": "integer", "minimum": 0, "maximum": 100, "position": 1 } + }, + "required": ["restaurantId", "grade"], + "additionalProperties": false + } +} +``` + +The `countable` / `summable` / `averageable` trio plus the three `range*` flags are the *prerequisites*; `rankedAverageable: true` is the one line that adds the ranking. That index's terminal property-name tree at `restaurantId` becomes a `ProvableCountProvableSumIndexedTree` carrying axes `[Avg]`. + +The count-only and sum-only shapes are correspondingly smaller: + +```json +{ "name": "byRestaurantVisits", + "properties": [{ "restaurantId": "asc" }], + "countable": "countable", "rangeCountable": true, "rankedCountable": true } +``` + +```json +{ "name": "byRestaurantTips", + "properties": [{ "restaurantId": "asc" }], + "summable": "amount", "rangeSummable": true, "rankedSummable": true } +``` + +which lay down a `ProvableCountIndexedTree` and a `ProvableSumIndexedTree` respectively. + +Note that the fixture puts each shape on its **own document type**. That's not an accident of style: two indexes over the same property set on one doctype is a `DuplicateIndexError`, so exercising all three variants needs three doctypes. + +### Choosing What to Set + +| You want | Set | +|---|---| +| Top / bottom K groups by document count | `rankedCountable: true` on a single-property, non-unique index that already has `countable` + `rangeCountable: true` | +| Top / bottom K groups by sum of a property | `rankedSummable: true` on an index with `summable: ""` + `rangeSummable: true` | +| Top / bottom K groups by average of a property | `rankedAverageable: true` on an index with `averageable: ""` + `rangeAverageable: true` (or the count+sum longhand) | +| Two rankings on one index (e.g. by count *and* by average) | Both keywords. The tree is a PCPSIT carrying both axes in its TLV; you pay one secondary Merk per axis on every write. | +| A ranking filtered by another property (`top 5 restaurants in London`) | Not available. Ranked indexes are single-property and ranked queries take no `where` clause — the secondary is sorted by aggregate, not by group key, so it cannot express a filtered subset. Model the filter as part of the grouping property, or rank client-side over a range query. | +| A ranking on a unique or contested index | Not available, and not meaningful: every group holds at most one document. | +| Range aggregates without ranking (the 4.0 surface) | Just the `range*` flags. Ranking is strictly additive — adding it never changes what a range query returns. | +| Nothing ranking-aware (default) | Don't set any `ranked*` flag. The terminal property-name tree keeps the type its range flags give it. | + +Every ranking axis is paid for on **every write** that touches a group, not just on the reads that use it. Two axes on one index means two secondary rewrites per document insert. Opt into the axes you will actually rank by. + +## What a Ranked Query Looks Like + +The query surface is deliberately narrow — one aggregate `select`, one `group_by`, one `ORDER BY` naming that select's aggregate, and a `LIMIT` (plus an optional `OFFSET`). Nothing else: + +```sql +SELECT avg(grade) FROM review + GROUP BY restaurantId + ORDER BY avg(grade) DESC + LIMIT 3 +``` + +That is ordinary SQL, deliberately. An earlier draft of this surface spelled the same question as `HAVING avg(grade) IN TOP(3)` — a `TOP` / `BOTTOM` primitive on the right of a `HAVING` clause. It was removed before release rather than deprecated: SQL already expresses "the n highest-scoring groups" with `ORDER BY` + `LIMIT`, and inventing a second, non-conformant spelling for it bought nothing but a grammar every client author would have to learn. `HAVING` is now purely a boolean per-group predicate, as in SQL. + +The [Ranked Index Examples](./ranked-index-examples.md) chapter covers the wire shape, the SDK surface, the proof, the rank/offset semantics, and the full list of what is rejected and why. diff --git a/book/src/drive/ranked-index-examples.md b/book/src/drive/ranked-index-examples.md new file mode 100644 index 00000000000..9c75d3b04b4 --- /dev/null +++ b/book/src/drive/ranked-index-examples.md @@ -0,0 +1,697 @@ +# Ranked Index Examples + +This chapter walks through a representative contract and shows how **ranked queries** work on Drive. Every example uses the **restaurants contract** at [`packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json`](https://github.com/dashpay/platform/blob/v4.2-dev/packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json) — the same fixture the write-path e2e suite and the query suite share. + +The chapter assumes you've read [Document Ranked Trees](./document-ranked-trees.md) for the storage layout, and the [Count](./count-index-examples.md) / [Sum](./sum-index-examples.md) / [Average](./average-index-examples.md) example chapters for the aggregate surfaces ranking builds on. Here we take the indexed-tree machinery as given and look at the query shape it enables: *"which `n` groups score highest?"* + +> **Status:** implemented and gated at protocol version 14. Unlike the count / sum / average chapters, this one is **not** backed by a worst-case bench — there is no ranked bench, so no measured proof sizes or timings appear below. Every value shown is instead taken from the end-to-end suite at [`packages/rs-drive/src/query/drive_document_ranked_query/tests.rs`](https://github.com/dashpay/platform/blob/v4.2-dev/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs), which runs prover and verifier against a live Drive and asserts the reconstructed root hash equals the database's own. Where a size is stated it is an asymptotic, not a measurement. + +## Why Ranking Is a Different Query Shape + +Every other aggregate query in Drive walks *value trees* under a property-name tree and aggregates what it finds: `AggregateCountOnRange` sums per-subtree counts along a boundary, the average carrier fans out over an `In` and aggregates each branch. A ranked query never touches the value trees at all. The answer already exists, pre-sorted, in the axis secondary described in the previous chapter — the query is a bounded scan of one end of it. + +Three consequences shape the entire API, and they are the reason this chapter's "what is rejected" table is longer than its query list: + +1. **No `where` clauses.** Ranked indexes are single-property, so there is no equality prefix to narrow; and a `where` on the ranked property itself asks for a *filtered* ranking, which the secondary cannot express — it is sorted by aggregate, not by group key. +2. **No `start_at` cursor.** A cursor names a document id, and document ids do not appear in a keyspace sorted by aggregate. `LIMIT` and `OFFSET` *are* honoured — they are how the ranking is sized and paged; see [Ranks and Offsets](#ranks-and-offsets). +3. **Entry order IS the ranking order.** The executor returns entries in the order grovedb walked the secondary. Callers must not re-sort. + +The rejections are **rejections rather than silent ignores**, on both the client and the server, because a ranked walk cannot honour them and silently answering a different question is worse than an error. + +### The grammar is plain SQL + +A ranked query is spelled the way SQL already spells "the n highest-scoring groups": + +```sql +SELECT avg(grade) FROM review + GROUP BY restaurantId + ORDER BY avg(grade) DESC + LIMIT 3 +``` + +`DESC` is the "top n" reading, `ASC` the "bottom n" reading. `LIMIT` is the ranking's size; `OFFSET` moves the window down the ranking. + +**This replaced a non-SQL spelling that never shipped.** An earlier draft put the ranking on the right of a `HAVING` clause — `HAVING avg(grade) IN TOP(3)`, with `TOP` / `BOTTOM` / `MAX` / `MIN` as cross-group primitives. It was removed before release rather than deprecated. The deliberate call: SQL conformance beats a bespoke primitive. Every client author already knows `ORDER BY … LIMIT`; nobody knows `IN TOP(n)`, and the two express exactly the same thing. The retired spelling also had a rough edge the SQL one simply does not have — `= MAX` means *every* group tied at the extreme, which a bounded read cannot prove, so `MAX` / `MIN` had to be permanently refused. `ORDER BY DESC LIMIT 1` is positional and has no such ambiguity. + +`HAVING` survives as what it is in SQL: a boolean per-group predicate. It is not yet evaluated (every non-empty `having` is `Unsupported`), and it cannot currently be combined with a ranking `ORDER BY` — the ranked executor reads a pre-sorted secondary and has no way to drop groups from the middle of that walk. + +## The Restaurants Contract + +Four document types, one per ranking shape. They all group by the same property (`restaurantId`) and each carries exactly one single-property index — because two indexes over the same property set on one doctype is a `DuplicateIndexError`, so exercising all three axes needs one doctype apiece. + +| doctype | index | declares | terminal property-name tree | +|---|---|---|---| +| `review` | `byRestaurant` | `averageable` + `rangeAverageable` + `rankedAverageable` | `ProvableCountProvableSumIndexedTree` axes `[Avg]` | +| `visit` | `byRestaurantVisits` | `countable` + `rangeCountable` + `rankedCountable` | `ProvableCountIndexedTree` | +| `tip` | `byRestaurantTips` | `summable` + `rangeSummable` + `rankedSummable` | `ProvableSumIndexedTree` | +| `adjustment` | `byRestaurantAdjustments` | same as `review` | `ProvableCountProvableSumIndexedTree` axes `[Avg]` | + +`adjustment` duplicates `review`'s shape for one reason: its aggregated property `delta` admits negative values, which `grade` (`minimum: 0`) does not, so it is the only doctype that can exercise signed sums and the floor-toward-negative-infinity rounding of the Avg sort key. + +```json +{ + "$formatVersion": "0", + "id": "AY6xWncZUFv2GCrS5seqKthUfbW9yYyUXtF8diSuHQ3f", + "ownerId": "AtirhSVpAWF7dEt6dLAmesC4Sr1MsJ9bFC1nLAoNnq2S", + "version": 1, + "documentSchemas": { + "review": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurant", + "properties": [ + { "restaurantId": "asc" } + ], + "countable": "countable", + "summable": "grade", + "averageable": "grade", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "grade": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "position": 1 + } + }, + "required": [ + "restaurantId", + "grade" + ], + "additionalProperties": false + }, + "visit": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurantVisits", + "properties": [ + { "restaurantId": "asc" } + ], + "countable": "countable", + "rangeCountable": true, + "rankedCountable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "guests": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "position": 1 + } + }, + "required": [ + "restaurantId", + "guests" + ], + "additionalProperties": false + }, + "tip": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurantTips", + "properties": [ + { "restaurantId": "asc" } + ], + "summable": "amount", + "rangeSummable": true, + "rankedSummable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "amount": { + "type": "integer", + "minimum": 0, + "maximum": 1000000, + "position": 1 + } + }, + "required": [ + "restaurantId", + "amount" + ], + "additionalProperties": false + }, + "adjustment": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurantAdjustments", + "properties": [ + { "restaurantId": "asc" } + ], + "countable": "countable", + "summable": "delta", + "averageable": "delta", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "delta": { + "type": "integer", + "minimum": -1000, + "maximum": 1000, + "position": 1 + } + }, + "required": [ + "restaurantId", + "delta" + ], + "additionalProperties": false + } + } +} +``` + +Three things to internalize before reading the queries: + +1. **`rankedAverageable: true` is one line on top of six prerequisite flags.** The `countable` / `summable` / `averageable` trio and the three `range*` flags are what give the terminal property-name tree its per-group `(count, sum)`; the ranked flag adds the ordered secondary that sorts those groups. Drop any prerequisite and the contract is rejected at parse time. +2. **`visit` needs no aggregated property.** The Count axis ranks by group *cardinality*, so `COUNT(*)` takes no field — and both the `select` and the `having` aggregate carry an **empty** field string on the wire. +3. **`tip` is sum-only.** It declares no count flags, so its terminal tree is a `ProvableSumIndexedTree` — you can rank restaurants by total tips, but not by tip *average*, because there is no count axis to divide by. Averages need both. + +## GroveDB Layout + +Each doctype's terminal property-name tree at `restaurantId` is an indexed tree: a primary Merk keyed by group key, plus one secondary per declared axis keyed by `(sort_key ‖ group_key)`. + +*Diagram conventions: blue is the indexed element wrapper; the primary Merk's children are the ordinary group value trees (green); the orange secondary holds one entry per group, aggregate-ordered.* + +```mermaid +flowchart TB + TD["@/contract_id/0x01/review"]:::tree + TD --> RID["restaurantId:
ProvableCountProvableSumIndexedTree axes [Avg]"]:::indexed + + RID --> PRIM["primary Merk
(keyed by group key)"]:::primary + RID --> SEC["Avg secondary Merk
(keyed by avg_fp_be16 ‖ group key)"]:::secondary + + PRIM --> GA["alpha: PCPS count=2 sum=170"]:::group + PRIM --> GB["beta: PCPS count=3 sum=180"]:::group + PRIM --> GD["delta: PCPS count=2 sum=60"]:::group + PRIM --> GG["gamma: PCPS count=1 sum=95"]:::group + + SEC --> SD["fp(30) ‖ delta"]:::leaf + SEC --> SB["fp(60) ‖ beta"]:::leaf + SEC --> SA["fp(85) ‖ alpha"]:::leaf + SEC --> SG["fp(95) ‖ gamma"]:::leaf + + classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px; + classDef indexed fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px; + classDef primary fill:#3fb950,color:#0d1117,stroke:#3fb950,stroke-width:2px; + classDef secondary fill:#fb8500,color:#0d1117,stroke:#fb8500,stroke-width:2px; + classDef group fill:#3fb950,color:#0d1117,stroke:#3fb950,stroke-width:2px; + classDef leaf fill:#21262d,color:#c9d1d9,stroke:#484f58; +``` + +The full grove path down to that indexed tree, for a single-property index: + +```text +[ RootTree::DataContractDocuments as u8 ] // 0x01 + / + / [ 0x01 ] // "documents", not "contract" + / // e.g. b"review" + / // e.g. b"restaurantId" +``` + +Every ranked read and every ranked proof is issued against exactly that path, built by the same `DriveDocumentRankedQuery::indexed_property_name_tree_path` on both sides — which is why prover and verifier cannot drift on *which* ranking is being checked. + +## The Request on the Wire + +Ranked queries ride the existing `GetDocumentsRequestV1`. No new request message and no new field were added: `selects`, `group_by`, `order_by`, `limit` and `offset` already existed, and the ranking is expressed by what goes in them. + +```proto +message GetDocumentsRequestV1 { + // … + repeated OrderClause order_by = 4; + optional uint32 limit = 5; + bool prove = 8; + repeated Select selects = 9; + repeated string group_by = 10; + optional uint32 offset = 12; +} +``` + +A well-formed ranked request carries **exactly one** select, `group_by` property and `order_by` clause, and everything else at its unset wire value: + +| Field | Ranked value | +|---|---| +| `selects` | one `Select { function: AVG, field: "grade" }` | +| `group_by` | `["restaurantId"]` | +| `order_by` | one `OrderClause { target: Field("grade"), ascending: false }` | +| `limit` | the ranking's `n`, `1 ..= 100` — **required** | +| `offset` | ranks to skip; unset = 0 | +| `where_clauses` | empty — **rejected if not** | +| `having` | empty — **rejected if not** | +| `start_after` / `start_at` | unset — **rejected if set** | + +### Naming the aggregate in `ORDER BY` + +The `order_by` clause names the aggregate it orders by through the wire's **field** target, and the name has to be the one the `select` already fixed: + +| `select` | `order_by` field | +|---|---| +| `SUM(amount)` | `"amount"` | +| `AVG(grade)` | `"grade"` | +| `COUNT(*)` | `"$count"` | + +`SUM(f)` / `AVG(f)` are named by `f` — the same property the projection aggregates, which is how `ORDER BY avg(grade)` reads once `SELECT` has fixed the function. `COUNT(*)` aggregates no property, so it is named by the reserved sentinel **`$count`** (`RANKED_COUNT_ORDER_KEY`). The `$` sigil is load-bearing: DPP reserves the `$` prefix for system properties (`$id`, `$ownerId`, …) and a schema cannot declare a property starting with it, so `$count` can never collide with a real column. A bare `count` would silently hijack ordering for any schema that happens to have a `count` field. + +An `order_by` naming anything else — a second clause, the `GROUP BY` property, an unrelated field — is rejected rather than normalized. The SDK's `order_by_selected_aggregate()` builder derives the name from the `select` through rs-drive's own mapping, so there is no way to get it wrong by hand. + +The wire also carries an explicit aggregate target (`OrderClause.target.aggregate`, e.g. `ORDER BY AVG(grade)` spelled out). It is still `Unsupported` — the field-target spelling above is the one the ranked executor reads — and exists so the explicit form can start being evaluated without another version bump. + +`limit` is bounded to `1 ..= 100` (`MAX_RANKED_LIMIT`). Out-of-range is **rejected, not clamped** — `k` is echoed inside the proof envelope and re-checked by the verifier, so a server-side clamp would produce a proof the client's own reconstruction rejects. + +## Ranks and Offsets + +`OFFSET` is honoured on the ranked path — the one place in the v1 surface where it is — and it is how you ask for a rank rather than a prefix: + +```sql +SELECT avg(grade) FROM review + GROUP BY restaurantId + ORDER BY avg(grade) DESC + LIMIT 1 OFFSET 4 -- the 5th-best restaurant +``` + +The response carries the skip back in `RankedEntries.skipped` (see [The Response](#the-response)), so the page is self-describing. + +`skipped` is the page's **starting rank base**: entry `i` is the group at rank `skipped + i` (0-based). Without it, a caller who asked for `LIMIT 1 OFFSET 4` receives one entry and has no way to tell that it really is the 5th-best group rather than the best. It is `0` for offset-less queries, which is what makes the field additive. + +Three properties worth stating plainly: + +- **The skip is attested, not walked.** grovedb proves the skipped region from the counted subtree commitments (`HashWithCount` / `HashWithCountAndSum`) rather than by traversing it. Both the prover's work and the proof's size stay `O(log n + k)` **at any offset**. +- **There is therefore no offset ceiling.** An offset of 4 and an offset of four billion cost the same, so there is no denial-of-service lever a cap would close — and a cap would only stop honest deep pagination. +- **An offset past the end is a positive answer.** `entries` comes back empty and `skipped` is the ranking's *entire attested population*. "There are only 12 groups" is more information than a bare empty list. + +On the **unproven** read there is nothing to attest and grovedb's read API does not report a short walk, so `skipped` simply echoes the requested offset. The proved and unproven paths therefore disagree in exactly one case — an offset past the end, where the unproven read reports the request and the proved one reports the truth. **Callers who need the population must prove.** + +## The Response + +The result is an additive `ResultData.ranked` variant: + +```proto +message RankedEntry { + bytes key = 1; + oneof value { + uint64 count = 2 [jstype = JS_STRING]; + sint64 sum = 3 [jstype = JS_STRING]; + double avg = 4; + } +} + +message RankedEntries { + repeated RankedEntry entries = 1; + optional uint64 skipped = 2 [jstype = JS_STRING]; +} +``` + +Five properties of the payload, each of them load-bearing: + +- **`skipped` is the page's starting rank** — see [Ranks and Offsets](#ranks-and-offsets). `0` for an offset-less query. +- **`key` is raw index-key bytes**, not a typed value — the same bytes that name the group's value tree under the index. For a `string` property that's its UTF-8 encoding (`b"alpha"`). Clients that want the typed value decode it with the document type's key deserialization; the wire carries bytes so prover and verifier agree without a schema round-trip. +- **Entry order IS the ranking order** — best-first for `DESC`, worst-first for `ASC`. Clients must not re-sort. Ties come back in group-key order *in the direction of the walk*, which is **descending** group-key order for `DESC`. +- **Fewer than `n` entries is normal**, not an error — the index simply has fewer groups than requested. +- **`avg` is a `double` approximation, and deliberately so.** What grovedb sorts the Avg axis by is an exact `i128` fixed point — `floor(sum × SCALE / count)` with euclidean (toward −∞) division — and this field is that integer divided by `RANKED_AVG_SCALE` in `f64`. The precision loss costs nothing because **`RankedEntry` only exists on the no-proof path**: a client that asked for a proof reconstructs each entry from the proof itself, where the exact fixed point lives, and never reads this field. So the wire says what it means — an approximation for the caller who already chose to trust the node — instead of dressing a trusted number up as an exact one. Two groups whose averages differ past `f64`'s ~15–16 significant digits can compare equal here; anything needing the committed integer must request the proof. Entry *order* is exact regardless: the ranking happened over the `i128` before the conversion. The decoder rejects a non-finite `avg`, or one that scales past `i128`, rather than casting it into a plausible-looking zero. + +## Queries in this Chapter + +Every query below uses the same shape — one aggregate select, one `group_by`, one `ORDER BY` on that aggregate and a `LIMIT` — and differs only in the axis and the ranking. All seven come from the end-to-end suite; the "verified result" rows are what the unproven read returned *and* what the verifier recovered from the proof, both asserted equal against the live grovedb root hash. + +| # | Query | Doctype / axis | Complexity | Verified result | +|---|-------|----------------|------------|-----------------| +| 1 | [Top 3 by Average Grade](#query-1--top-3-restaurants-by-average-grade) | `review` / Avg | O(log G + k) | `gamma(95)`, `alpha(85)`, `beta(60)` | +| 2 | [The Worst Average](#query-2--the-worst-average-and-the-fixed-point-floor) | `review` / Avg | O(log G + 1) | `epsilon(10.5)` | +| 3 | [Top 2 by Visit Count](#query-3--top-2-restaurants-by-visit-count) | `visit` / Count | O(log G + k) | `delta(4)`, `beta(3)` | +| 4 | [The Quietest Restaurant](#query-4--the-quietest-restaurant) | `visit` / Count | O(log G + 1) | `alpha(1)` | +| 5 | [Bottom 3 by Tip Total](#query-5--bottom-3-restaurants-by-tip-total) | `tip` / Sum | O(log G + k) | `delta(1)`, `gamma(24)`, `alpha(25)` | +| 6 | [Four-Way Tie](#query-6--a-four-way-tie) | `tip` / Sum | O(log G + k) | `gamma`, `delta`, `beta`, `alpha` | +| 7 | [More Than Exist](#query-7--asking-for-more-groups-than-exist) | `tip` / Sum | O(log G + G) | `beta`, `alpha` (2 of a requested 100) | + +**Complexity variable.** `G` = the number of distinct groups (distinct values of the ranked property). Notably absent: the total document count `N`. A ranked walk reads pre-committed per-group aggregates out of the secondary and never enumerates documents, so both work and proof size are `O(log G + k)` — independent of how many documents sit inside the groups it returns. Proof bytes grow linearly in `k` (one committed secondary entry per returned group) plus the `O(log G)` ancestor path, which is exactly why `k` is capped at 100. + +## Query 1 — Top 3 Restaurants by Average Grade + +Eight `review` documents across four restaurants: + +| restaurant | grades | count | sum | average | +|---|---|---|---|---| +| `alpha` | 90, 80 | 2 | 170 | 85 | +| `beta` | 60, 70, 50 | 3 | 180 | 60 | +| `gamma` | 95 | 1 | 95 | 95 | +| `delta` | 40, 20 | 2 | 60 | 30 | + +```text +select = AVG(grade) +group_by = [restaurantId] +order_by = grade DESC +limit = 3 +prove = true +``` + +**Path query:** + +```text +path: ["@", contract_id, 0x01, "review", "restaurantId"] +read: indexed_avg_top_k(k = 3, descending = true) +``` + +**Verified result** (returned by `GroveDb::verify_indexed_axis_top_k`, wrapped by `DriveDocumentRankedQuery::verify_ranked_top_k_proof`): + +```text +[ ("gamma", AvgFixedPoint(95 × SCALE)), + ("alpha", AvgFixedPoint(85 × SCALE)), + ("beta", AvgFixedPoint(60 × SCALE)) ] + +descending by average: gamma(95) > alpha(85) > beta(60) > delta(30) +``` + +`delta` is below the cut and never appears in the proof — that is the whole point. A count-tree walk would have had to commit all four groups to convince the client which three are highest; the secondary's ordering means committing three entries plus the boundary is sufficient. + +The client divides: `95 × SCALE / SCALE = 95`. It never sees a `double` off the wire. + +## Query 2 — The Worst Average (and the Fixed-Point Floor) + +Add a fifth restaurant whose sum doesn't divide evenly: `epsilon` with grades 10 and 11. + +```text +select = AVG(grade) +group_by = [restaurantId] +order_by = grade ASC +limit = 1 +prove = true +``` + +**Verified result:** + +```text +[ ("epsilon", AvgFixedPoint(21 × SCALE / 2)) ] + +21 / 2 = 10.5 is the lowest average +``` + +Three things this query pins: + +- **`ORDER BY … ASC LIMIT 1` is the positional single worst-ranked group.** It walks the secondary from the smallest sort key up and stops after one entry. +- **The sort key is `floor(sum × SCALE / count)`**, computed with grovedb's own `compute_avg_fixed_point` — the test asserts the returned value against both the hand-written `21 × SCALE / 2` *and* grovedb's function, so a change to either the scale or the rounding shows up immediately. +- **`as_f64()` divides back down**, returning exactly `10.5`. It is a display helper: lossy for large counts and sums, and never to be used for consensus-relevant comparisons, since two groups whose fixed-point averages differ can round to the same `f64`. + +For the negative half of the rounding story, the `adjustment` doctype exists: with `delta` admitting negatives, `expected_avg_fixed_point(-11, 3)` must come out exactly one fixed-point bucket *below* what truncating division would give, because euclidean division floors toward −∞ while Rust's `/` truncates toward zero. + +## Query 3 — Top 2 Restaurants by Visit Count + +Ten `visit` documents. The `guests` values are stored but irrelevant here — the Count axis ranks by how many documents are in each group, not by anything inside them: + +| restaurant | visits | count | +|---|---|---| +| `delta` | 4 documents | 4 | +| `beta` | 3 documents | 3 | +| `gamma` | 2 documents | 2 | +| `alpha` | 1 document | 1 | + +```text +select = COUNT(*) +group_by = [restaurantId] +order_by = $count DESC +limit = 2 +prove = true +``` + +**Verified result:** + +```text +[ ("delta", Count(4)), + ("beta", Count(3)) ] + +descending by document count: delta(4) > beta(3) > gamma(2) > alpha(1) +``` + +On the wire, both the select and the having aggregate carry `field: ""`. A non-empty field on a `COUNT` ranking is rejected — `COUNT(field)` (counting non-null values of a property) is a different aggregate and the ranked surface doesn't serve it. + +Note that `visit`'s index declares no sum flags at all, so its terminal tree is a plain `ProvableCountIndexedTree` — a single secondary, no axes TLV. Asking for `SUM(guests)` or `AVG(guests)` against it resolves no index and is rejected: the stored element *could* host that secondary, but the contract never declared it, and the write path therefore never maintained it. + +## Query 4 — The Quietest Restaurant + +```text +select = COUNT(*) +group_by = [restaurantId] +order_by = $count ASC +limit = 1 +prove = true +``` + +**Verified result:** + +```text +[ ("alpha", Count(1)) ] +``` + +Note what this query does **not** claim. `ORDER BY $count ASC LIMIT 1` is the *positional* single worst-ranked group: if two restaurants tied at one visit each, one of them comes back and the other does not, deterministically (ties break by group key in the walk's direction). It is not "every group at the minimum" — that is a set a bounded read cannot attest, and it is why the retired grammar's `= MIN` spelling could never have been served. The positional reading documents dropping ties as its meaning; the value-based one would have had to lie about it. + +See [What Is Rejected and Why](#what-is-rejected-and-why) for the full reasoning. + +## Query 5 — Bottom 3 Restaurants by Tip Total + +Seven `tip` documents: + +| restaurant | amounts | sum | +|---|---|---| +| `beta` | 100 | 100 | +| `alpha` | 10, 15 | 25 | +| `gamma` | 7, 8, 9 | 24 | +| `delta` | 1 | 1 | + +```text +select = SUM(amount) +group_by = [restaurantId] +order_by = amount ASC +limit = 3 +prove = true +``` + +**Verified result:** + +```text +[ ("delta", Sum(1)), + ("gamma", Sum(24)), + ("alpha", Sum(25)) ] + +ascending by sum: delta(1) < gamma(24) < alpha(25) < beta(100) +``` + +`ORDER BY amount DESC LIMIT 1` on the same data returns `[("beta", Sum(100))]`. + +Sums are signed (`sint64` on the wire, `i64` in the SDK) for the same reason the sum surface's are: grovedb's sum trees model overflow into negative space rather than saturating. The Sum axis's sort key is the `i64` with its sign bit flipped, which is what makes plain byte comparison order negatives below positives. + +## Query 6 — A Four-Way Tie + +Four groups, all summing to 50. Only the group key distinguishes them. + +```text +select = SUM(amount) +group_by = [restaurantId] +order_by = amount DESC // and the ASC mirror +limit = 4 +``` + +**Verified results:** + +```text +DESC LIMIT 4: ["gamma", "delta", "beta", "alpha"] // descending group key +ASC LIMIT 4: ["alpha", "beta", "delta", "gamma"] // ascending group key +``` + +The two directions are exact reverses of each other under a full-width `k`. That falls out of the secondary's key layout rather than from a separate tie-break rule: keys are `(sort_key ‖ group_key)`, and the walk is a plain directional scan of that keyspace, so equal sort keys come back in group-key order *in the direction of the walk*. + +The consequence that matters is determinism under truncation. `DESC LIMIT 2` on the same data returns `["gamma", "delta"]` — a **specific** subset, not an arbitrary one, and the same subset on every node. That reproducibility is what makes a tie-truncating `LIMIT k` provable at all, and it is the reason the retired `= MAX` spelling could never have been served: `= MAX` means *every* group at the extreme, and a bounded read cannot attest that nothing else ties. + +## Query 7 — Asking For More Groups Than Exist + +Two `tip` groups, `LIMIT 100`: + +```text +select = SUM(amount) +group_by = [restaurantId] +order_by = amount DESC +limit = 100 +prove = true +``` + +**Verified result:** + +```text +[ ("beta", Sum(20)), ("alpha", Sum(10)) ] +``` + +A short result is the index having fewer groups, not an error, and the proof round-trips just the same. The verifier enforces the bound in the other direction only: **at most** `k` entries, because more would mean the proof committed a longer walk than the request authorized. + +## Fetching From the Rust SDK + +`DocumentRankedEntries` lands on the standard `Fetch` trait against a `DocumentQuery`, with `order_by_selected_aggregate()` building the one ordering clause the surface needs: + +```rust,no_run +use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}}; +use dash_sdk::drive::query::SelectProjection; +use dash_sdk::platform::documents::document_query::RankingDirection; +use drive_proof_verifier::{DocumentRankedEntries, RankedEntryValue, RANKED_AVG_SCALE}; +use futures::executor::block_on; + +# const RESTAURANTS_CONTRACT_ID: [u8; 32] = [0; 32]; +let sdk = Sdk::new_mock(); +let contract = block_on(DataContract::fetch(&sdk, Identifier::new(RESTAURANTS_CONTRACT_ID))) + .expect("fetch contract") + .expect("contract exists"); + +let query = DocumentQuery::new(contract, "review") + .expect("document type exists") + .with_select(SelectProjection::avg("grade")) + .with_group_by("restaurantId") + .order_by_selected_aggregate(RankingDirection::Descending) + .with_limit(5); + +let ranked = block_on(DocumentRankedEntries::fetch(&sdk, query)) + .expect("fetch succeeds") + .expect("a well-formed ranked query always answers"); + +// Entry order IS the ranking order — best first. +for (offset, entry) in ranked.entries.iter().enumerate() { + let rank = ranked.starting_rank + offset as u64; + let restaurant = String::from_utf8_lossy(&entry.key); + if let RankedEntryValue::AvgFixedPoint(fixed_point) = entry.value { + let average = (fixed_point as f64) / (RANKED_AVG_SCALE as f64); + println!("#{}: {restaurant}: {average}", rank + 1); + } +} +``` + +`fixed_point` is the exact integer the proof commits to on this (proved) path. On a `prove = false` fetch the wire carries only the `double` — the SDK re-scales it back into the same variant, so the low digits are reconstruction noise. See the [response notes](#the-response). + +The **5th-best restaurant** is the same query with the window moved down one rank at a time: + +```rust,no_run +# use dash_sdk::platform::{DataContract, DocumentQuery}; +# use dash_sdk::platform::documents::document_query::RankingDirection; +# use dash_sdk::drive::query::SelectProjection; +# fn example(contract: DataContract) -> Result<(), dash_sdk::Error> { +// SELECT avg(grade) GROUP BY restaurantId +// ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4 +let query = DocumentQuery::new(contract, "review")? + .with_select(SelectProjection::avg("grade")) + .with_group_by("restaurantId") + .order_by_selected_aggregate(RankingDirection::Descending) + .with_limit(1) + .with_offset(4); +# Ok(()) +# } +``` + +Notes on the surface: + +- **`order_by_selected_aggregate()` derives the ordered field from the `select`** through rs-drive's own key mapping — `"grade"` for `AVG(grade)`, the `$count` sentinel for `COUNT(*)`. You never name it by hand, so client and server cannot disagree about what is being ordered. Set the `select` first; the builder reads it. +- **It replaces rather than appends.** A ranked query takes exactly one ordering clause. +- **`RANKED_AVG_SCALE` is a re-export of grovedb's constant**, which moved from `10^15` to `10^19` before release. Never hardcode the literal. `RankedEntryValue::as_f64()` does the same division for display purposes. +- **`ranked.entries` is `Vec`** in ranking order. Do not re-sort it. **`ranked.starting_rank`** is the rank of `entries[0]`, re-derived from the proof rather than taken from the node. +- **The `Fetch` path always requests a proof.** There is no `prove` knob on it; if you need the unproven read, that's the `DocumentRankedEntries::from_unproved_response` path. +- **No JS/WASM or FFI binding exists yet.** The ranked surface is Rust-SDK-only today; the generated gRPC types are present in the web client but nothing is hand-written on top of them. + +## Proof Notes + +**The root hash is the whole point.** The merk-level verifier returning `Ok` is not by itself evidence of anything. A bit-flip sweep over a real ranked envelope shows that most mutations do error out — but roughly **9%** of them (bytes of sibling-subtree hashes inside the ancestor layer proofs) verify cleanly and return the *correct* entries, under a **different** reconstructed root hash. What rejects those is the tenderdash composition: `drive_proof_verifier::verify_ranked_top_k_proof` checks the reconstructed root against the quorum-signed app hash for the response's block, and there is no path through it that yields entries without that check having run. + +Three things grovedb checks before the entries come back: + +1. **The envelope's `(axis, k, descending, offset)` match the query.** They are echoed in the proof and compared against the arguments, so a proof generated for a different ranking — or for a different page of the same one — is rejected rather than silently reinterpreted. +2. **The result's axis shape matches the requested axis** — a `Count` request must not come back holding `Sum` entries. Belt-and-braces on top of (1). +3. **At most `k` entries.** Fewer is normal; more would mean the proof committed a longer walk than the request authorized. + +**Empty rankings prove.** An earlier iteration of this surface could not prove one: grovedb's non-paginated prover had no absence-proof shape for "this axis secondary has no entries", so the merk layer failed with `Cannot create proof for empty tree` and the node had to map that to an `invalid_argument` telling the caller to retry unproved. It was reachable by anyone — querying a freshly registered contract with `prove = true` did it. + +The paginated prover (`prove_indexed_axis_top_k_paginated`) closed that gap: against an empty axis secondary it emits a **guaranteed-empty range** rather than refusing. Proving a ranking over an index with no documents now succeeds and returns an empty page, so the proved and unproven paths agree on the one case where they used to diverge. There is no fallback to implement and no rejection to recognise; `prove = true` is always answerable. + +The same mechanism is what makes an offset past the end provable — see [Ranks and Offsets](#ranks-and-offsets). The node keeps a narrow backstop mapping for the old merk-level error because it names a *class* of failure rather than a single call site, but it is no longer a live path. + +**Against a protocol-version-13 node**, the whole request is rejected as `Unsupported` — v13's query table has no ranked path and refuses the aggregate ordering. That is the intended activation gate, not a bug: a v13 node and a v14 node must disagree here and nowhere else, which is what lets a mixed-version network run through the upgrade. + +## What Is Rejected and Why + +Everything below is rejected *before* any grovedb work, and most of it is mirrored client-side so the caller learns without a round trip. + +| Rejected | Why | +|---|---| +| **Compound (multi-property) ranked index** — at contract-parse time, `ranked aggregates are only supported on single-property indexes in this protocol version` | Two reasons. A compound index whose prefix level also terminates an aggregating index would need its ranked terminal tree wrapped in a `NonCounted` / `NotSummed` shell — and the storage layer structurally rejects any wrapper around an indexed tree, because the wrapper would neutralize the very aggregates the ranking indexes. Separately, the ranked query surface has no equality-prefix routing yet. Both are relaxable at a future protocol version. The query-side index picker is the backstop: it refuses compound indexes even if the flags are somehow present. | +| **`unique` ranked index** — `ranked aggregates are not supported on unique indexes: each group of a unique index contains at most one document, so there is nothing meaningful to rank` | Every ranking over a unique index degenerates to a constant-per-group ordering a plain range query already serves, while still paying for an indexed tree and its secondary maintenance on every write. | +| **Contested ranked index** | Covered transitively — a contested index is unique by construction, so it hits the check above. | +| **`where` clauses** — `InvalidWhereClauseComponents` | Ranked indexes are single-property, so there is no equality prefix to narrow; and a clause on the ranked property itself asks for a ranking over a filtered subset, which the secondary cannot answer because it is ordered by aggregate rather than by group key. Silently dropping the filter would return the global ranking under the guise of a filtered one. | +| **`start_at` / `start_after`** — `InvalidLimit` | The cursor names a document id, but a ranked walk iterates an aggregate-ordered keyspace in which document ids do not appear. | +| **`order_by` naming anything but the selected aggregate**, or more than one clause — `InvalidParameter` | The single ordering clause *is* the ranking, and the secondary is sorted by one aggregate only. An ordering on the `GROUP BY` property, on an unrelated field, or a second tie-break clause names an order the secondary cannot produce. Accepting and silently ignoring it is the one genuinely dangerous option. Use the aggregate's own name (`$count` for `COUNT(*)`), or flip `ASC` ↔ `DESC` to reverse the ranking. | +| **`group_by` with ≠ 1 property** — `InvalidParameter` | Ranked indexes are single-property, so there is no compound grouping to rank over. | +| **any non-empty `having`** — `Unsupported` | `HAVING` is a boolean per-group predicate and is not evaluated at any protocol version. It also cannot combine with a ranking `ORDER BY`: the ranked executor reads a pre-sorted secondary and has no way to drop groups from the middle of that walk. | +| **no `order_by` at all, on a grouped aggregate** — routed elsewhere | Without an ordering this is a plain grouped aggregate, not a ranking; the caller wanted the `DocumentSplitCounts` / `DocumentSplitSums` / `DocumentSplitAverages` surface. | +| **`COUNT(field)`** (non-`*`) — `Unsupported`; **`SUM` / `AVG` with an empty field** — `InvalidParameter` | The Count axis ranks group cardinality and takes no field; the Sum and Avg axes rank the property the index accumulates and require it. | +| **`limit` unset, `0`, or `> 100`** — `InvalidLimit` | A ranking with no `n` has no size, and `LIMIT 0` selects nothing. The ceiling is a **hard limit, not a clamp**, because `k` is echoed in the proof envelope and re-checked by the verifier — a silent clamp would produce a proof the client's own reconstruction rejects. | +| **A `SUM` / `AVG` ranking on a field the index doesn't accumulate** — no covering index | The picker requires the select's field to be the index's `summable` property. Resolving anything else would answer about the wrong property with no indication that a substitution happened. | +| ~~**Proving a ranking over an empty index**~~ | **No longer rejected.** The paginated prover emits a guaranteed-empty range against an empty axis secondary, so `prove = true` over an index with no documents returns an empty page. Listed here because it used to be a rejection and the old advice ("retry with `prove = false`") is now wrong. | + +## At-a-Glance Comparison + +| Query | Doctype | Terminal tree | Axis | Ranking | Returned variant | +|---|---|---|---|---|---| +| 1 — Top 3 by average | `review` | `ProvableCountProvableSumIndexedTree [Avg]` | Avg | `ORDER BY grade DESC LIMIT 3` | `AvgFixedPoint(i128)` | +| 2 — Worst average | `review` | same | Avg | `ORDER BY grade ASC LIMIT 1` | `AvgFixedPoint(i128)` | +| 3 — Top 2 by visits | `visit` | `ProvableCountIndexedTree` | Count | `ORDER BY $count DESC LIMIT 2` | `Count(u64)` | +| 4 — Quietest | `visit` | same | Count | `ORDER BY $count ASC LIMIT 1` | `Count(u64)` | +| 5 — Bottom 3 by tips | `tip` | `ProvableSumIndexedTree` | Sum | `ORDER BY amount ASC LIMIT 3` | `Sum(i64)` | +| 6 — Four-way tie | `tip` | same | Sum | `ORDER BY amount DESC/ASC LIMIT 4` | `Sum(i64)` | +| 7 — More than exist | `tip` | same | Sum | `ORDER BY amount DESC LIMIT 100` | `Sum(i64)` (2 entries) | + +Every row is one bounded scan of one secondary Merk, one proof, one root-hash commit. The shape never varies with the axis — only the sort-key width (8 / 8 / 16 bytes) and the returned scalar type do. + +## What's Next + +Two capabilities are deliberately deferred and would each land as a separate protocol-version change: + +- **Compound ranked indexes.** Both blockers are named above — the indexed-tree wrapper conflict and the missing equality-prefix routing. Lifting them would enable "top 5 chefs at restaurant `alpha` by average grade" without a client-side sort. +- **Offset-paginated rankings.** grovedb already has the primitive (`prove_indexed_axis_top_k_paginated`); it is not exposed because a ranked query currently rejects every pagination knob, and wiring one in would need the result-size contract (`result size == n`) to be rewritten rather than extended. + +For the shape of the tree these queries read, and the write-path cost of maintaining it, see [Document Ranked Trees](./document-ranked-trees.md). diff --git a/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js b/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js index 7d382e919fb..0d1e656f487 100644 --- a/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js +++ b/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js @@ -21233,271 +21233,6 @@ $root.org = (function() { return HavingAggregate; })(); - GetDocumentsRequest.HavingRanking = (function() { - - /** - * Properties of a HavingRanking. - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest - * @interface IHavingRanking - * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind|null} [kind] HavingRanking kind - * @property {number|Long|null} [n] HavingRanking n - */ - - /** - * Constructs a new HavingRanking. - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest - * @classdesc Represents a HavingRanking. - * @implements IHavingRanking - * @constructor - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking=} [properties] Properties to set - */ - function HavingRanking(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * HavingRanking kind. - * @member {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} kind - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @instance - */ - HavingRanking.prototype.kind = 0; - - /** - * HavingRanking n. - * @member {number|Long} n - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @instance - */ - HavingRanking.prototype.n = $util.Long ? $util.Long.fromBits(0,0,true) : 0; - - /** - * Creates a new HavingRanking instance using the specified properties. - * @function create - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking=} [properties] Properties to set - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking instance - */ - HavingRanking.create = function create(properties) { - return new HavingRanking(properties); - }; - - /** - * Encodes the specified HavingRanking message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.verify|verify} messages. - * @function encode - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking} message HavingRanking message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - HavingRanking.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - if (message.kind != null && Object.hasOwnProperty.call(message, "kind")) - writer.uint32(/* id 1, wireType 0 =*/8).int32(message.kind); - if (message.n != null && Object.hasOwnProperty.call(message, "n")) - writer.uint32(/* id 2, wireType 0 =*/16).uint64(message.n); - return writer; - }; - - /** - * Encodes the specified HavingRanking message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.verify|verify} messages. - * @function encodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking} message HavingRanking message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - HavingRanking.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a HavingRanking message from the specified reader or buffer. - * @function decode - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - HavingRanking.decode = function decode(reader, length) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking(); - while (reader.pos < end) { - var tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - message.kind = reader.int32(); - break; - case 2: - message.n = reader.uint64(); - break; - default: - reader.skipType(tag & 7); - break; - } - } - return message; - }; - - /** - * Decodes a HavingRanking message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - HavingRanking.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a HavingRanking message. - * @function verify - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - HavingRanking.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (message.kind != null && message.hasOwnProperty("kind")) - switch (message.kind) { - default: - return "kind: enum value expected"; - case 0: - case 1: - case 2: - case 3: - break; - } - if (message.n != null && message.hasOwnProperty("n")) - if (!$util.isInteger(message.n) && !(message.n && $util.isInteger(message.n.low) && $util.isInteger(message.n.high))) - return "n: integer|Long expected"; - return null; - }; - - /** - * Creates a HavingRanking message from a plain object. Also converts values to their respective internal types. - * @function fromObject - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {Object.} object Plain object - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking - */ - HavingRanking.fromObject = function fromObject(object) { - if (object instanceof $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking) - return object; - var message = new $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking(); - switch (object.kind) { - case "MIN": - case 0: - message.kind = 0; - break; - case "MAX": - case 1: - message.kind = 1; - break; - case "TOP": - case 2: - message.kind = 2; - break; - case "BOTTOM": - case 3: - message.kind = 3; - break; - } - if (object.n != null) - if ($util.Long) - (message.n = $util.Long.fromValue(object.n)).unsigned = true; - else if (typeof object.n === "string") - message.n = parseInt(object.n, 10); - else if (typeof object.n === "number") - message.n = object.n; - else if (typeof object.n === "object") - message.n = new $util.LongBits(object.n.low >>> 0, object.n.high >>> 0).toNumber(true); - return message; - }; - - /** - * Creates a plain object from a HavingRanking message. Also converts values to other types if specified. - * @function toObject - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} message HavingRanking - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - HavingRanking.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - object.kind = options.enums === String ? "MIN" : 0; - if ($util.Long) { - var long = new $util.Long(0, 0, true); - object.n = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; - } else - object.n = options.longs === String ? "0" : 0; - } - if (message.kind != null && message.hasOwnProperty("kind")) - object.kind = options.enums === String ? $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind[message.kind] : message.kind; - if (message.n != null && message.hasOwnProperty("n")) - if (typeof message.n === "number") - object.n = options.longs === String ? String(message.n) : message.n; - else - object.n = options.longs === String ? $util.Long.prototype.toString.call(message.n) : options.longs === Number ? new $util.LongBits(message.n.low >>> 0, message.n.high >>> 0).toNumber(true) : message.n; - return object; - }; - - /** - * Converts this HavingRanking to JSON. - * @function toJSON - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @instance - * @returns {Object.} JSON object - */ - HavingRanking.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - /** - * Kind enum. - * @name org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind - * @enum {number} - * @property {number} MIN=0 MIN value - * @property {number} MAX=1 MAX value - * @property {number} TOP=2 TOP value - * @property {number} BOTTOM=3 BOTTOM value - */ - HavingRanking.Kind = (function() { - var valuesById = {}, values = Object.create(valuesById); - values[valuesById[0] = "MIN"] = 0; - values[valuesById[1] = "MAX"] = 1; - values[valuesById[2] = "TOP"] = 2; - values[valuesById[3] = "BOTTOM"] = 3; - return values; - })(); - - return HavingRanking; - })(); - GetDocumentsRequest.HavingClause = (function() { /** @@ -21507,7 +21242,6 @@ $root.org = (function() { * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingAggregate|null} [aggregate] HavingClause aggregate * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator|null} [operator] HavingClause operator * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.IDocumentFieldValue|null} [value] HavingClause value - * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking|null} [ranking] HavingClause ranking */ /** @@ -21549,25 +21283,17 @@ $root.org = (function() { */ HavingClause.prototype.value = null; - /** - * HavingClause ranking. - * @member {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking|null|undefined} ranking - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause - * @instance - */ - HavingClause.prototype.ranking = null; - // OneOf field names bound to virtual getters and setters var $oneOfFields; /** * HavingClause right. - * @member {"value"|"ranking"|undefined} right + * @member {"value"|undefined} right * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause * @instance */ Object.defineProperty(HavingClause.prototype, "right", { - get: $util.oneOfGetter($oneOfFields = ["value", "ranking"]), + get: $util.oneOfGetter($oneOfFields = ["value"]), set: $util.oneOfSetter($oneOfFields) }); @@ -21601,8 +21327,6 @@ $root.org = (function() { writer.uint32(/* id 2, wireType 0 =*/16).int32(message.operator); if (message.value != null && Object.hasOwnProperty.call(message, "value")) $root.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.encode(message.value, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); - if (message.ranking != null && Object.hasOwnProperty.call(message, "ranking")) - $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.encode(message.ranking, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); return writer; }; @@ -21646,9 +21370,6 @@ $root.org = (function() { case 3: message.value = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.decode(reader, reader.uint32()); break; - case 4: - message.ranking = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.decode(reader, reader.uint32()); - break; default: reader.skipType(tag & 7); break; @@ -21715,16 +21436,6 @@ $root.org = (function() { return "value." + error; } } - if (message.ranking != null && message.hasOwnProperty("ranking")) { - if (properties.right === 1) - return "right: multiple values"; - properties.right = 1; - { - var error = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.verify(message.ranking); - if (error) - return "ranking." + error; - } - } return null; }; @@ -21796,11 +21507,6 @@ $root.org = (function() { throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.value: object expected"); message.value = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.fromObject(object.value); } - if (object.ranking != null) { - if (typeof object.ranking !== "object") - throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.ranking: object expected"); - message.ranking = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.fromObject(object.ranking); - } return message; }; @@ -21830,11 +21536,6 @@ $root.org = (function() { if (options.oneofs) object.right = "value"; } - if (message.ranking != null && message.hasOwnProperty("ranking")) { - object.ranking = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject(message.ranking, options); - if (options.oneofs) - object.right = "ranking"; - } return object; }; @@ -27016,27 +26717,27 @@ $root.org = (function() { return AverageResults; })(); - GetDocumentsResponseV1.ResultData = (function() { + GetDocumentsResponseV1.RankedEntry = (function() { /** - * Properties of a ResultData. + * Properties of a RankedEntry. * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 - * @interface IResultData - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null} [documents] ResultData documents - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null} [counts] ResultData counts - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null} [sums] ResultData sums - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null} [averages] ResultData averages + * @interface IRankedEntry + * @property {Uint8Array|null} [key] RankedEntry key + * @property {number|Long|null} [count] RankedEntry count + * @property {number|Long|null} [sum] RankedEntry sum + * @property {number|null} [avg] RankedEntry avg */ /** - * Constructs a new ResultData. + * Constructs a new RankedEntry. * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 - * @classdesc Represents a ResultData. - * @implements IResultData + * @classdesc Represents a RankedEntry. + * @implements IRankedEntry * @constructor - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry=} [properties] Properties to set */ - function ResultData(properties) { + function RankedEntry(properties) { if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -27044,128 +26745,128 @@ $root.org = (function() { } /** - * ResultData documents. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null|undefined} documents - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry key. + * @member {Uint8Array} key + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.documents = null; + RankedEntry.prototype.key = $util.newBuffer([]); /** - * ResultData counts. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null|undefined} counts - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry count. + * @member {number|Long} count + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.counts = null; + RankedEntry.prototype.count = $util.Long ? $util.Long.fromBits(0,0,true) : 0; /** - * ResultData sums. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null|undefined} sums - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry sum. + * @member {number|Long} sum + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.sums = null; + RankedEntry.prototype.sum = $util.Long ? $util.Long.fromBits(0,0,false) : 0; /** - * ResultData averages. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null|undefined} averages - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry avg. + * @member {number} avg + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.averages = null; + RankedEntry.prototype.avg = 0; // OneOf field names bound to virtual getters and setters var $oneOfFields; /** - * ResultData variant. - * @member {"documents"|"counts"|"sums"|"averages"|undefined} variant - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry value. + * @member {"count"|"sum"|"avg"|undefined} value + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - Object.defineProperty(ResultData.prototype, "variant", { - get: $util.oneOfGetter($oneOfFields = ["documents", "counts", "sums", "averages"]), + Object.defineProperty(RankedEntry.prototype, "value", { + get: $util.oneOfGetter($oneOfFields = ["count", "sum", "avg"]), set: $util.oneOfSetter($oneOfFields) }); /** - * Creates a new ResultData instance using the specified properties. + * Creates a new RankedEntry instance using the specified properties. * @function create - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set - * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData instance + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry instance */ - ResultData.create = function create(properties) { - return new ResultData(properties); + RankedEntry.create = function create(properties) { + return new RankedEntry(properties); }; /** - * Encodes the specified ResultData message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * Encodes the specified RankedEntry message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.verify|verify} messages. * @function encode - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry} message RankedEntry message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ - ResultData.encode = function encode(message, writer) { + RankedEntry.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); - if (message.documents != null && Object.hasOwnProperty.call(message, "documents")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.encode(message.documents, writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); - if (message.counts != null && Object.hasOwnProperty.call(message, "counts")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.encode(message.counts, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); - if (message.sums != null && Object.hasOwnProperty.call(message, "sums")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.encode(message.sums, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); - if (message.averages != null && Object.hasOwnProperty.call(message, "averages")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.encode(message.averages, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.key != null && Object.hasOwnProperty.call(message, "key")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.key); + if (message.count != null && Object.hasOwnProperty.call(message, "count")) + writer.uint32(/* id 2, wireType 0 =*/16).uint64(message.count); + if (message.sum != null && Object.hasOwnProperty.call(message, "sum")) + writer.uint32(/* id 3, wireType 0 =*/24).sint64(message.sum); + if (message.avg != null && Object.hasOwnProperty.call(message, "avg")) + writer.uint32(/* id 4, wireType 1 =*/33).double(message.avg); return writer; }; /** - * Encodes the specified ResultData message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * Encodes the specified RankedEntry message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.verify|verify} messages. * @function encodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry} message RankedEntry message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ - ResultData.encodeDelimited = function encodeDelimited(message, writer) { + RankedEntry.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** - * Decodes a ResultData message from the specified reader or buffer. + * Decodes a RankedEntry message from the specified reader or buffer. * @function decode - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand - * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - ResultData.decode = function decode(reader, length) { + RankedEntry.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData(); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry(); while (reader.pos < end) { var tag = reader.uint32(); switch (tag >>> 3) { case 1: - message.documents = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.decode(reader, reader.uint32()); + message.key = reader.bytes(); break; case 2: - message.counts = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.decode(reader, reader.uint32()); + message.count = reader.uint64(); break; case 3: - message.sums = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.decode(reader, reader.uint32()); + message.sum = reader.sint64(); break; case 4: - message.averages = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.decode(reader, reader.uint32()); + message.avg = reader.double(); break; default: reader.skipType(tag & 7); @@ -27176,36 +26877,608 @@ $root.org = (function() { }; /** - * Decodes a ResultData message from the specified reader or buffer, length delimited. + * Decodes a RankedEntry message from the specified reader or buffer, length delimited. * @function decodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - ResultData.decodeDelimited = function decodeDelimited(reader) { + RankedEntry.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** - * Verifies a ResultData message. + * Verifies a RankedEntry message. * @function verify - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ - ResultData.verify = function verify(message) { + RankedEntry.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; var properties = {}; - if (message.documents != null && message.hasOwnProperty("documents")) { - properties.variant = 1; - { + if (message.key != null && message.hasOwnProperty("key")) + if (!(message.key && typeof message.key.length === "number" || $util.isString(message.key))) + return "key: buffer expected"; + if (message.count != null && message.hasOwnProperty("count")) { + properties.value = 1; + if (!$util.isInteger(message.count) && !(message.count && $util.isInteger(message.count.low) && $util.isInteger(message.count.high))) + return "count: integer|Long expected"; + } + if (message.sum != null && message.hasOwnProperty("sum")) { + if (properties.value === 1) + return "value: multiple values"; + properties.value = 1; + if (!$util.isInteger(message.sum) && !(message.sum && $util.isInteger(message.sum.low) && $util.isInteger(message.sum.high))) + return "sum: integer|Long expected"; + } + if (message.avg != null && message.hasOwnProperty("avg")) { + if (properties.value === 1) + return "value: multiple values"; + properties.value = 1; + if (typeof message.avg !== "number") + return "avg: number expected"; + } + return null; + }; + + /** + * Creates a RankedEntry message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry + */ + RankedEntry.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry(); + if (object.key != null) + if (typeof object.key === "string") + $util.base64.decode(object.key, message.key = $util.newBuffer($util.base64.length(object.key)), 0); + else if (object.key.length >= 0) + message.key = object.key; + if (object.count != null) + if ($util.Long) + (message.count = $util.Long.fromValue(object.count)).unsigned = true; + else if (typeof object.count === "string") + message.count = parseInt(object.count, 10); + else if (typeof object.count === "number") + message.count = object.count; + else if (typeof object.count === "object") + message.count = new $util.LongBits(object.count.low >>> 0, object.count.high >>> 0).toNumber(true); + if (object.sum != null) + if ($util.Long) + (message.sum = $util.Long.fromValue(object.sum)).unsigned = false; + else if (typeof object.sum === "string") + message.sum = parseInt(object.sum, 10); + else if (typeof object.sum === "number") + message.sum = object.sum; + else if (typeof object.sum === "object") + message.sum = new $util.LongBits(object.sum.low >>> 0, object.sum.high >>> 0).toNumber(); + if (object.avg != null) + message.avg = Number(object.avg); + return message; + }; + + /** + * Creates a plain object from a RankedEntry message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} message RankedEntry + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + RankedEntry.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) + if (options.bytes === String) + object.key = ""; + else { + object.key = []; + if (options.bytes !== Array) + object.key = $util.newBuffer(object.key); + } + if (message.key != null && message.hasOwnProperty("key")) + object.key = options.bytes === String ? $util.base64.encode(message.key, 0, message.key.length) : options.bytes === Array ? Array.prototype.slice.call(message.key) : message.key; + if (message.count != null && message.hasOwnProperty("count")) { + if (typeof message.count === "number") + object.count = options.longs === String ? String(message.count) : message.count; + else + object.count = options.longs === String ? $util.Long.prototype.toString.call(message.count) : options.longs === Number ? new $util.LongBits(message.count.low >>> 0, message.count.high >>> 0).toNumber(true) : message.count; + if (options.oneofs) + object.value = "count"; + } + if (message.sum != null && message.hasOwnProperty("sum")) { + if (typeof message.sum === "number") + object.sum = options.longs === String ? String(message.sum) : message.sum; + else + object.sum = options.longs === String ? $util.Long.prototype.toString.call(message.sum) : options.longs === Number ? new $util.LongBits(message.sum.low >>> 0, message.sum.high >>> 0).toNumber() : message.sum; + if (options.oneofs) + object.value = "sum"; + } + if (message.avg != null && message.hasOwnProperty("avg")) { + object.avg = options.json && !isFinite(message.avg) ? String(message.avg) : message.avg; + if (options.oneofs) + object.value = "avg"; + } + return object; + }; + + /** + * Converts this RankedEntry to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry + * @instance + * @returns {Object.} JSON object + */ + RankedEntry.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return RankedEntry; + })(); + + GetDocumentsResponseV1.RankedEntries = (function() { + + /** + * Properties of a RankedEntries. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @interface IRankedEntries + * @property {Array.|null} [entries] RankedEntries entries + * @property {number|Long|null} [skipped] RankedEntries skipped + */ + + /** + * Constructs a new RankedEntries. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @classdesc Represents a RankedEntries. + * @implements IRankedEntries + * @constructor + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries=} [properties] Properties to set + */ + function RankedEntries(properties) { + this.entries = []; + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * RankedEntries entries. + * @member {Array.} entries + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @instance + */ + RankedEntries.prototype.entries = $util.emptyArray; + + /** + * RankedEntries skipped. + * @member {number|Long} skipped + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @instance + */ + RankedEntries.prototype.skipped = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * Creates a new RankedEntries instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries instance + */ + RankedEntries.create = function create(properties) { + return new RankedEntries(properties); + }; + + /** + * Encodes the specified RankedEntries message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries} message RankedEntries message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RankedEntries.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.entries != null && message.entries.length) + for (var i = 0; i < message.entries.length; ++i) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.encode(message.entries[i], writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); + if (message.skipped != null && Object.hasOwnProperty.call(message, "skipped")) + writer.uint32(/* id 2, wireType 0 =*/16).uint64(message.skipped); + return writer; + }; + + /** + * Encodes the specified RankedEntries message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries} message RankedEntries message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RankedEntries.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a RankedEntries message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RankedEntries.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (!(message.entries && message.entries.length)) + message.entries = []; + message.entries.push($root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.decode(reader, reader.uint32())); + break; + case 2: + message.skipped = reader.uint64(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a RankedEntries message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RankedEntries.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a RankedEntries message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + RankedEntries.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.entries != null && message.hasOwnProperty("entries")) { + if (!Array.isArray(message.entries)) + return "entries: array expected"; + for (var i = 0; i < message.entries.length; ++i) { + var error = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.verify(message.entries[i]); + if (error) + return "entries." + error; + } + } + if (message.skipped != null && message.hasOwnProperty("skipped")) + if (!$util.isInteger(message.skipped) && !(message.skipped && $util.isInteger(message.skipped.low) && $util.isInteger(message.skipped.high))) + return "skipped: integer|Long expected"; + return null; + }; + + /** + * Creates a RankedEntries message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries + */ + RankedEntries.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries(); + if (object.entries) { + if (!Array.isArray(object.entries)) + throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.entries: array expected"); + message.entries = []; + for (var i = 0; i < object.entries.length; ++i) { + if (typeof object.entries[i] !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.entries: object expected"); + message.entries[i] = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.fromObject(object.entries[i]); + } + } + if (object.skipped != null) + if ($util.Long) + (message.skipped = $util.Long.fromValue(object.skipped)).unsigned = true; + else if (typeof object.skipped === "string") + message.skipped = parseInt(object.skipped, 10); + else if (typeof object.skipped === "number") + message.skipped = object.skipped; + else if (typeof object.skipped === "object") + message.skipped = new $util.LongBits(object.skipped.low >>> 0, object.skipped.high >>> 0).toNumber(true); + return message; + }; + + /** + * Creates a plain object from a RankedEntries message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} message RankedEntries + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + RankedEntries.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.arrays || options.defaults) + object.entries = []; + if (options.defaults) + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.skipped = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.skipped = options.longs === String ? "0" : 0; + if (message.entries && message.entries.length) { + object.entries = []; + for (var j = 0; j < message.entries.length; ++j) + object.entries[j] = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject(message.entries[j], options); + } + if (message.skipped != null && message.hasOwnProperty("skipped")) + if (typeof message.skipped === "number") + object.skipped = options.longs === String ? String(message.skipped) : message.skipped; + else + object.skipped = options.longs === String ? $util.Long.prototype.toString.call(message.skipped) : options.longs === Number ? new $util.LongBits(message.skipped.low >>> 0, message.skipped.high >>> 0).toNumber(true) : message.skipped; + return object; + }; + + /** + * Converts this RankedEntries to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @instance + * @returns {Object.} JSON object + */ + RankedEntries.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return RankedEntries; + })(); + + GetDocumentsResponseV1.ResultData = (function() { + + /** + * Properties of a ResultData. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @interface IResultData + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null} [documents] ResultData documents + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null} [counts] ResultData counts + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null} [sums] ResultData sums + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null} [averages] ResultData averages + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries|null} [ranked] ResultData ranked + */ + + /** + * Constructs a new ResultData. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @classdesc Represents a ResultData. + * @implements IResultData + * @constructor + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set + */ + function ResultData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * ResultData documents. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null|undefined} documents + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.documents = null; + + /** + * ResultData counts. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null|undefined} counts + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.counts = null; + + /** + * ResultData sums. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null|undefined} sums + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.sums = null; + + /** + * ResultData averages. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null|undefined} averages + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.averages = null; + + /** + * ResultData ranked. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries|null|undefined} ranked + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.ranked = null; + + // OneOf field names bound to virtual getters and setters + var $oneOfFields; + + /** + * ResultData variant. + * @member {"documents"|"counts"|"sums"|"averages"|"ranked"|undefined} variant + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + Object.defineProperty(ResultData.prototype, "variant", { + get: $util.oneOfGetter($oneOfFields = ["documents", "counts", "sums", "averages", "ranked"]), + set: $util.oneOfSetter($oneOfFields) + }); + + /** + * Creates a new ResultData instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData instance + */ + ResultData.create = function create(properties) { + return new ResultData(properties); + }; + + /** + * Encodes the specified ResultData message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ResultData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.documents != null && Object.hasOwnProperty.call(message, "documents")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.encode(message.documents, writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); + if (message.counts != null && Object.hasOwnProperty.call(message, "counts")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.encode(message.counts, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); + if (message.sums != null && Object.hasOwnProperty.call(message, "sums")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.encode(message.sums, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); + if (message.averages != null && Object.hasOwnProperty.call(message, "averages")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.encode(message.averages, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.ranked != null && Object.hasOwnProperty.call(message, "ranked")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.encode(message.ranked, writer.uint32(/* id 5, wireType 2 =*/42).fork()).ldelim(); + return writer; + }; + + /** + * Encodes the specified ResultData message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ResultData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a ResultData message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ResultData.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.documents = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.decode(reader, reader.uint32()); + break; + case 2: + message.counts = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.decode(reader, reader.uint32()); + break; + case 3: + message.sums = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.decode(reader, reader.uint32()); + break; + case 4: + message.averages = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.decode(reader, reader.uint32()); + break; + case 5: + message.ranked = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.decode(reader, reader.uint32()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a ResultData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ResultData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a ResultData message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + ResultData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + var properties = {}; + if (message.documents != null && message.hasOwnProperty("documents")) { + properties.variant = 1; + { var error = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.verify(message.documents); if (error) return "documents." + error; @@ -27241,6 +27514,16 @@ $root.org = (function() { return "averages." + error; } } + if (message.ranked != null && message.hasOwnProperty("ranked")) { + if (properties.variant === 1) + return "variant: multiple values"; + properties.variant = 1; + { + var error = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.verify(message.ranked); + if (error) + return "ranked." + error; + } + } return null; }; @@ -27276,6 +27559,11 @@ $root.org = (function() { throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.averages: object expected"); message.averages = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.fromObject(object.averages); } + if (object.ranked != null) { + if (typeof object.ranked !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.ranked: object expected"); + message.ranked = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.fromObject(object.ranked); + } return message; }; @@ -27312,6 +27600,11 @@ $root.org = (function() { if (options.oneofs) object.variant = "averages"; } + if (message.ranked != null && message.hasOwnProperty("ranked")) { + object.ranked = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject(message.ranked, options); + if (options.oneofs) + object.variant = "ranked"; + } return object; }; diff --git a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js index 727887cb44c..978be7d0743 100644 --- a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js +++ b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js @@ -20725,271 +20725,6 @@ $root.org = (function() { return HavingAggregate; })(); - GetDocumentsRequest.HavingRanking = (function() { - - /** - * Properties of a HavingRanking. - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest - * @interface IHavingRanking - * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind|null} [kind] HavingRanking kind - * @property {number|Long|null} [n] HavingRanking n - */ - - /** - * Constructs a new HavingRanking. - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest - * @classdesc Represents a HavingRanking. - * @implements IHavingRanking - * @constructor - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking=} [properties] Properties to set - */ - function HavingRanking(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * HavingRanking kind. - * @member {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} kind - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @instance - */ - HavingRanking.prototype.kind = 0; - - /** - * HavingRanking n. - * @member {number|Long} n - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @instance - */ - HavingRanking.prototype.n = $util.Long ? $util.Long.fromBits(0,0,true) : 0; - - /** - * Creates a new HavingRanking instance using the specified properties. - * @function create - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking=} [properties] Properties to set - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking instance - */ - HavingRanking.create = function create(properties) { - return new HavingRanking(properties); - }; - - /** - * Encodes the specified HavingRanking message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.verify|verify} messages. - * @function encode - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking} message HavingRanking message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - HavingRanking.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - if (message.kind != null && Object.hasOwnProperty.call(message, "kind")) - writer.uint32(/* id 1, wireType 0 =*/8).int32(message.kind); - if (message.n != null && Object.hasOwnProperty.call(message, "n")) - writer.uint32(/* id 2, wireType 0 =*/16).uint64(message.n); - return writer; - }; - - /** - * Encodes the specified HavingRanking message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.verify|verify} messages. - * @function encodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking} message HavingRanking message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - HavingRanking.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a HavingRanking message from the specified reader or buffer. - * @function decode - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - HavingRanking.decode = function decode(reader, length) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking(); - while (reader.pos < end) { - var tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - message.kind = reader.int32(); - break; - case 2: - message.n = reader.uint64(); - break; - default: - reader.skipType(tag & 7); - break; - } - } - return message; - }; - - /** - * Decodes a HavingRanking message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - HavingRanking.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a HavingRanking message. - * @function verify - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - HavingRanking.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (message.kind != null && message.hasOwnProperty("kind")) - switch (message.kind) { - default: - return "kind: enum value expected"; - case 0: - case 1: - case 2: - case 3: - break; - } - if (message.n != null && message.hasOwnProperty("n")) - if (!$util.isInteger(message.n) && !(message.n && $util.isInteger(message.n.low) && $util.isInteger(message.n.high))) - return "n: integer|Long expected"; - return null; - }; - - /** - * Creates a HavingRanking message from a plain object. Also converts values to their respective internal types. - * @function fromObject - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {Object.} object Plain object - * @returns {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} HavingRanking - */ - HavingRanking.fromObject = function fromObject(object) { - if (object instanceof $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking) - return object; - var message = new $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking(); - switch (object.kind) { - case "MIN": - case 0: - message.kind = 0; - break; - case "MAX": - case 1: - message.kind = 1; - break; - case "TOP": - case 2: - message.kind = 2; - break; - case "BOTTOM": - case 3: - message.kind = 3; - break; - } - if (object.n != null) - if ($util.Long) - (message.n = $util.Long.fromValue(object.n)).unsigned = true; - else if (typeof object.n === "string") - message.n = parseInt(object.n, 10); - else if (typeof object.n === "number") - message.n = object.n; - else if (typeof object.n === "object") - message.n = new $util.LongBits(object.n.low >>> 0, object.n.high >>> 0).toNumber(true); - return message; - }; - - /** - * Creates a plain object from a HavingRanking message. Also converts values to other types if specified. - * @function toObject - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} message HavingRanking - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - HavingRanking.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - object.kind = options.enums === String ? "MIN" : 0; - if ($util.Long) { - var long = new $util.Long(0, 0, true); - object.n = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; - } else - object.n = options.longs === String ? "0" : 0; - } - if (message.kind != null && message.hasOwnProperty("kind")) - object.kind = options.enums === String ? $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind[message.kind] : message.kind; - if (message.n != null && message.hasOwnProperty("n")) - if (typeof message.n === "number") - object.n = options.longs === String ? String(message.n) : message.n; - else - object.n = options.longs === String ? $util.Long.prototype.toString.call(message.n) : options.longs === Number ? new $util.LongBits(message.n.low >>> 0, message.n.high >>> 0).toNumber(true) : message.n; - return object; - }; - - /** - * Converts this HavingRanking to JSON. - * @function toJSON - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking - * @instance - * @returns {Object.} JSON object - */ - HavingRanking.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - /** - * Kind enum. - * @name org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind - * @enum {number} - * @property {number} MIN=0 MIN value - * @property {number} MAX=1 MAX value - * @property {number} TOP=2 TOP value - * @property {number} BOTTOM=3 BOTTOM value - */ - HavingRanking.Kind = (function() { - var valuesById = {}, values = Object.create(valuesById); - values[valuesById[0] = "MIN"] = 0; - values[valuesById[1] = "MAX"] = 1; - values[valuesById[2] = "TOP"] = 2; - values[valuesById[3] = "BOTTOM"] = 3; - return values; - })(); - - return HavingRanking; - })(); - GetDocumentsRequest.HavingClause = (function() { /** @@ -20999,7 +20734,6 @@ $root.org = (function() { * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingAggregate|null} [aggregate] HavingClause aggregate * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator|null} [operator] HavingClause operator * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.IDocumentFieldValue|null} [value] HavingClause value - * @property {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking|null} [ranking] HavingClause ranking */ /** @@ -21041,25 +20775,17 @@ $root.org = (function() { */ HavingClause.prototype.value = null; - /** - * HavingClause ranking. - * @member {org.dash.platform.dapi.v0.GetDocumentsRequest.IHavingRanking|null|undefined} ranking - * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause - * @instance - */ - HavingClause.prototype.ranking = null; - // OneOf field names bound to virtual getters and setters var $oneOfFields; /** * HavingClause right. - * @member {"value"|"ranking"|undefined} right + * @member {"value"|undefined} right * @memberof org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause * @instance */ Object.defineProperty(HavingClause.prototype, "right", { - get: $util.oneOfGetter($oneOfFields = ["value", "ranking"]), + get: $util.oneOfGetter($oneOfFields = ["value"]), set: $util.oneOfSetter($oneOfFields) }); @@ -21093,8 +20819,6 @@ $root.org = (function() { writer.uint32(/* id 2, wireType 0 =*/16).int32(message.operator); if (message.value != null && Object.hasOwnProperty.call(message, "value")) $root.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.encode(message.value, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); - if (message.ranking != null && Object.hasOwnProperty.call(message, "ranking")) - $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.encode(message.ranking, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); return writer; }; @@ -21138,9 +20862,6 @@ $root.org = (function() { case 3: message.value = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.decode(reader, reader.uint32()); break; - case 4: - message.ranking = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.decode(reader, reader.uint32()); - break; default: reader.skipType(tag & 7); break; @@ -21207,16 +20928,6 @@ $root.org = (function() { return "value." + error; } } - if (message.ranking != null && message.hasOwnProperty("ranking")) { - if (properties.right === 1) - return "right: multiple values"; - properties.right = 1; - { - var error = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.verify(message.ranking); - if (error) - return "ranking." + error; - } - } return null; }; @@ -21288,11 +20999,6 @@ $root.org = (function() { throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.value: object expected"); message.value = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.fromObject(object.value); } - if (object.ranking != null) { - if (typeof object.ranking !== "object") - throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.ranking: object expected"); - message.ranking = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.fromObject(object.ranking); - } return message; }; @@ -21322,11 +21028,6 @@ $root.org = (function() { if (options.oneofs) object.right = "value"; } - if (message.ranking != null && message.hasOwnProperty("ranking")) { - object.ranking = $root.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject(message.ranking, options); - if (options.oneofs) - object.right = "ranking"; - } return object; }; @@ -26508,27 +26209,27 @@ $root.org = (function() { return AverageResults; })(); - GetDocumentsResponseV1.ResultData = (function() { + GetDocumentsResponseV1.RankedEntry = (function() { /** - * Properties of a ResultData. + * Properties of a RankedEntry. * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 - * @interface IResultData - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null} [documents] ResultData documents - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null} [counts] ResultData counts - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null} [sums] ResultData sums - * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null} [averages] ResultData averages + * @interface IRankedEntry + * @property {Uint8Array|null} [key] RankedEntry key + * @property {number|Long|null} [count] RankedEntry count + * @property {number|Long|null} [sum] RankedEntry sum + * @property {number|null} [avg] RankedEntry avg */ /** - * Constructs a new ResultData. + * Constructs a new RankedEntry. * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 - * @classdesc Represents a ResultData. - * @implements IResultData + * @classdesc Represents a RankedEntry. + * @implements IRankedEntry * @constructor - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry=} [properties] Properties to set */ - function ResultData(properties) { + function RankedEntry(properties) { if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -26536,128 +26237,128 @@ $root.org = (function() { } /** - * ResultData documents. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null|undefined} documents - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry key. + * @member {Uint8Array} key + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.documents = null; + RankedEntry.prototype.key = $util.newBuffer([]); /** - * ResultData counts. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null|undefined} counts - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry count. + * @member {number|Long} count + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.counts = null; + RankedEntry.prototype.count = $util.Long ? $util.Long.fromBits(0,0,true) : 0; /** - * ResultData sums. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null|undefined} sums - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry sum. + * @member {number|Long} sum + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.sums = null; + RankedEntry.prototype.sum = $util.Long ? $util.Long.fromBits(0,0,false) : 0; /** - * ResultData averages. - * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null|undefined} averages - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry avg. + * @member {number} avg + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - ResultData.prototype.averages = null; + RankedEntry.prototype.avg = 0; // OneOf field names bound to virtual getters and setters var $oneOfFields; /** - * ResultData variant. - * @member {"documents"|"counts"|"sums"|"averages"|undefined} variant - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * RankedEntry value. + * @member {"count"|"sum"|"avg"|undefined} value + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @instance */ - Object.defineProperty(ResultData.prototype, "variant", { - get: $util.oneOfGetter($oneOfFields = ["documents", "counts", "sums", "averages"]), + Object.defineProperty(RankedEntry.prototype, "value", { + get: $util.oneOfGetter($oneOfFields = ["count", "sum", "avg"]), set: $util.oneOfSetter($oneOfFields) }); /** - * Creates a new ResultData instance using the specified properties. + * Creates a new RankedEntry instance using the specified properties. * @function create - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set - * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData instance + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry instance */ - ResultData.create = function create(properties) { - return new ResultData(properties); + RankedEntry.create = function create(properties) { + return new RankedEntry(properties); }; /** - * Encodes the specified ResultData message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * Encodes the specified RankedEntry message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.verify|verify} messages. * @function encode - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry} message RankedEntry message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ - ResultData.encode = function encode(message, writer) { + RankedEntry.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); - if (message.documents != null && Object.hasOwnProperty.call(message, "documents")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.encode(message.documents, writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); - if (message.counts != null && Object.hasOwnProperty.call(message, "counts")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.encode(message.counts, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); - if (message.sums != null && Object.hasOwnProperty.call(message, "sums")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.encode(message.sums, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); - if (message.averages != null && Object.hasOwnProperty.call(message, "averages")) - $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.encode(message.averages, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.key != null && Object.hasOwnProperty.call(message, "key")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.key); + if (message.count != null && Object.hasOwnProperty.call(message, "count")) + writer.uint32(/* id 2, wireType 0 =*/16).uint64(message.count); + if (message.sum != null && Object.hasOwnProperty.call(message, "sum")) + writer.uint32(/* id 3, wireType 0 =*/24).sint64(message.sum); + if (message.avg != null && Object.hasOwnProperty.call(message, "avg")) + writer.uint32(/* id 4, wireType 1 =*/33).double(message.avg); return writer; }; /** - * Encodes the specified ResultData message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * Encodes the specified RankedEntry message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.verify|verify} messages. * @function encodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static - * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntry} message RankedEntry message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ - ResultData.encodeDelimited = function encodeDelimited(message, writer) { + RankedEntry.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** - * Decodes a ResultData message from the specified reader or buffer. + * Decodes a RankedEntry message from the specified reader or buffer. * @function decode - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand - * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - ResultData.decode = function decode(reader, length) { + RankedEntry.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData(); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry(); while (reader.pos < end) { var tag = reader.uint32(); switch (tag >>> 3) { case 1: - message.documents = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.decode(reader, reader.uint32()); + message.key = reader.bytes(); break; case 2: - message.counts = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.decode(reader, reader.uint32()); + message.count = reader.uint64(); break; case 3: - message.sums = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.decode(reader, reader.uint32()); + message.sum = reader.sint64(); break; case 4: - message.averages = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.decode(reader, reader.uint32()); + message.avg = reader.double(); break; default: reader.skipType(tag & 7); @@ -26668,36 +26369,608 @@ $root.org = (function() { }; /** - * Decodes a ResultData message from the specified reader or buffer, length delimited. + * Decodes a RankedEntry message from the specified reader or buffer, length delimited. * @function decodeDelimited - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - ResultData.decodeDelimited = function decodeDelimited(reader) { + RankedEntry.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** - * Verifies a ResultData message. + * Verifies a RankedEntry message. * @function verify - * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ - ResultData.verify = function verify(message) { + RankedEntry.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; var properties = {}; - if (message.documents != null && message.hasOwnProperty("documents")) { - properties.variant = 1; - { + if (message.key != null && message.hasOwnProperty("key")) + if (!(message.key && typeof message.key.length === "number" || $util.isString(message.key))) + return "key: buffer expected"; + if (message.count != null && message.hasOwnProperty("count")) { + properties.value = 1; + if (!$util.isInteger(message.count) && !(message.count && $util.isInteger(message.count.low) && $util.isInteger(message.count.high))) + return "count: integer|Long expected"; + } + if (message.sum != null && message.hasOwnProperty("sum")) { + if (properties.value === 1) + return "value: multiple values"; + properties.value = 1; + if (!$util.isInteger(message.sum) && !(message.sum && $util.isInteger(message.sum.low) && $util.isInteger(message.sum.high))) + return "sum: integer|Long expected"; + } + if (message.avg != null && message.hasOwnProperty("avg")) { + if (properties.value === 1) + return "value: multiple values"; + properties.value = 1; + if (typeof message.avg !== "number") + return "avg: number expected"; + } + return null; + }; + + /** + * Creates a RankedEntry message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} RankedEntry + */ + RankedEntry.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry(); + if (object.key != null) + if (typeof object.key === "string") + $util.base64.decode(object.key, message.key = $util.newBuffer($util.base64.length(object.key)), 0); + else if (object.key.length >= 0) + message.key = object.key; + if (object.count != null) + if ($util.Long) + (message.count = $util.Long.fromValue(object.count)).unsigned = true; + else if (typeof object.count === "string") + message.count = parseInt(object.count, 10); + else if (typeof object.count === "number") + message.count = object.count; + else if (typeof object.count === "object") + message.count = new $util.LongBits(object.count.low >>> 0, object.count.high >>> 0).toNumber(true); + if (object.sum != null) + if ($util.Long) + (message.sum = $util.Long.fromValue(object.sum)).unsigned = false; + else if (typeof object.sum === "string") + message.sum = parseInt(object.sum, 10); + else if (typeof object.sum === "number") + message.sum = object.sum; + else if (typeof object.sum === "object") + message.sum = new $util.LongBits(object.sum.low >>> 0, object.sum.high >>> 0).toNumber(); + if (object.avg != null) + message.avg = Number(object.avg); + return message; + }; + + /** + * Creates a plain object from a RankedEntry message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} message RankedEntry + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + RankedEntry.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) + if (options.bytes === String) + object.key = ""; + else { + object.key = []; + if (options.bytes !== Array) + object.key = $util.newBuffer(object.key); + } + if (message.key != null && message.hasOwnProperty("key")) + object.key = options.bytes === String ? $util.base64.encode(message.key, 0, message.key.length) : options.bytes === Array ? Array.prototype.slice.call(message.key) : message.key; + if (message.count != null && message.hasOwnProperty("count")) { + if (typeof message.count === "number") + object.count = options.longs === String ? String(message.count) : message.count; + else + object.count = options.longs === String ? $util.Long.prototype.toString.call(message.count) : options.longs === Number ? new $util.LongBits(message.count.low >>> 0, message.count.high >>> 0).toNumber(true) : message.count; + if (options.oneofs) + object.value = "count"; + } + if (message.sum != null && message.hasOwnProperty("sum")) { + if (typeof message.sum === "number") + object.sum = options.longs === String ? String(message.sum) : message.sum; + else + object.sum = options.longs === String ? $util.Long.prototype.toString.call(message.sum) : options.longs === Number ? new $util.LongBits(message.sum.low >>> 0, message.sum.high >>> 0).toNumber() : message.sum; + if (options.oneofs) + object.value = "sum"; + } + if (message.avg != null && message.hasOwnProperty("avg")) { + object.avg = options.json && !isFinite(message.avg) ? String(message.avg) : message.avg; + if (options.oneofs) + object.value = "avg"; + } + return object; + }; + + /** + * Converts this RankedEntry to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry + * @instance + * @returns {Object.} JSON object + */ + RankedEntry.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return RankedEntry; + })(); + + GetDocumentsResponseV1.RankedEntries = (function() { + + /** + * Properties of a RankedEntries. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @interface IRankedEntries + * @property {Array.|null} [entries] RankedEntries entries + * @property {number|Long|null} [skipped] RankedEntries skipped + */ + + /** + * Constructs a new RankedEntries. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @classdesc Represents a RankedEntries. + * @implements IRankedEntries + * @constructor + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries=} [properties] Properties to set + */ + function RankedEntries(properties) { + this.entries = []; + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * RankedEntries entries. + * @member {Array.} entries + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @instance + */ + RankedEntries.prototype.entries = $util.emptyArray; + + /** + * RankedEntries skipped. + * @member {number|Long} skipped + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @instance + */ + RankedEntries.prototype.skipped = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * Creates a new RankedEntries instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries instance + */ + RankedEntries.create = function create(properties) { + return new RankedEntries(properties); + }; + + /** + * Encodes the specified RankedEntries message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries} message RankedEntries message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RankedEntries.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.entries != null && message.entries.length) + for (var i = 0; i < message.entries.length; ++i) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.encode(message.entries[i], writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); + if (message.skipped != null && Object.hasOwnProperty.call(message, "skipped")) + writer.uint32(/* id 2, wireType 0 =*/16).uint64(message.skipped); + return writer; + }; + + /** + * Encodes the specified RankedEntries message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries} message RankedEntries message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RankedEntries.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a RankedEntries message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RankedEntries.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (!(message.entries && message.entries.length)) + message.entries = []; + message.entries.push($root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.decode(reader, reader.uint32())); + break; + case 2: + message.skipped = reader.uint64(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a RankedEntries message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RankedEntries.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a RankedEntries message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + RankedEntries.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.entries != null && message.hasOwnProperty("entries")) { + if (!Array.isArray(message.entries)) + return "entries: array expected"; + for (var i = 0; i < message.entries.length; ++i) { + var error = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.verify(message.entries[i]); + if (error) + return "entries." + error; + } + } + if (message.skipped != null && message.hasOwnProperty("skipped")) + if (!$util.isInteger(message.skipped) && !(message.skipped && $util.isInteger(message.skipped.low) && $util.isInteger(message.skipped.high))) + return "skipped: integer|Long expected"; + return null; + }; + + /** + * Creates a RankedEntries message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} RankedEntries + */ + RankedEntries.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries(); + if (object.entries) { + if (!Array.isArray(object.entries)) + throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.entries: array expected"); + message.entries = []; + for (var i = 0; i < object.entries.length; ++i) { + if (typeof object.entries[i] !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.entries: object expected"); + message.entries[i] = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.fromObject(object.entries[i]); + } + } + if (object.skipped != null) + if ($util.Long) + (message.skipped = $util.Long.fromValue(object.skipped)).unsigned = true; + else if (typeof object.skipped === "string") + message.skipped = parseInt(object.skipped, 10); + else if (typeof object.skipped === "number") + message.skipped = object.skipped; + else if (typeof object.skipped === "object") + message.skipped = new $util.LongBits(object.skipped.low >>> 0, object.skipped.high >>> 0).toNumber(true); + return message; + }; + + /** + * Creates a plain object from a RankedEntries message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} message RankedEntries + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + RankedEntries.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.arrays || options.defaults) + object.entries = []; + if (options.defaults) + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.skipped = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.skipped = options.longs === String ? "0" : 0; + if (message.entries && message.entries.length) { + object.entries = []; + for (var j = 0; j < message.entries.length; ++j) + object.entries[j] = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject(message.entries[j], options); + } + if (message.skipped != null && message.hasOwnProperty("skipped")) + if (typeof message.skipped === "number") + object.skipped = options.longs === String ? String(message.skipped) : message.skipped; + else + object.skipped = options.longs === String ? $util.Long.prototype.toString.call(message.skipped) : options.longs === Number ? new $util.LongBits(message.skipped.low >>> 0, message.skipped.high >>> 0).toNumber(true) : message.skipped; + return object; + }; + + /** + * Converts this RankedEntries to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries + * @instance + * @returns {Object.} JSON object + */ + RankedEntries.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return RankedEntries; + })(); + + GetDocumentsResponseV1.ResultData = (function() { + + /** + * Properties of a ResultData. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @interface IResultData + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null} [documents] ResultData documents + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null} [counts] ResultData counts + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null} [sums] ResultData sums + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null} [averages] ResultData averages + * @property {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries|null} [ranked] ResultData ranked + */ + + /** + * Constructs a new ResultData. + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1 + * @classdesc Represents a ResultData. + * @implements IResultData + * @constructor + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set + */ + function ResultData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * ResultData documents. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IDocuments|null|undefined} documents + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.documents = null; + + /** + * ResultData counts. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ICountResults|null|undefined} counts + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.counts = null; + + /** + * ResultData sums. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ISumResults|null|undefined} sums + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.sums = null; + + /** + * ResultData averages. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IAverageResults|null|undefined} averages + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.averages = null; + + /** + * ResultData ranked. + * @member {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IRankedEntries|null|undefined} ranked + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + ResultData.prototype.ranked = null; + + // OneOf field names bound to virtual getters and setters + var $oneOfFields; + + /** + * ResultData variant. + * @member {"documents"|"counts"|"sums"|"averages"|"ranked"|undefined} variant + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @instance + */ + Object.defineProperty(ResultData.prototype, "variant", { + get: $util.oneOfGetter($oneOfFields = ["documents", "counts", "sums", "averages", "ranked"]), + set: $util.oneOfSetter($oneOfFields) + }); + + /** + * Creates a new ResultData instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData instance + */ + ResultData.create = function create(properties) { + return new ResultData(properties); + }; + + /** + * Encodes the specified ResultData message. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ResultData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.documents != null && Object.hasOwnProperty.call(message, "documents")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.encode(message.documents, writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); + if (message.counts != null && Object.hasOwnProperty.call(message, "counts")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.encode(message.counts, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); + if (message.sums != null && Object.hasOwnProperty.call(message, "sums")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.encode(message.sums, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); + if (message.averages != null && Object.hasOwnProperty.call(message, "averages")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.encode(message.averages, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.ranked != null && Object.hasOwnProperty.call(message, "ranked")) + $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.encode(message.ranked, writer.uint32(/* id 5, wireType 2 =*/42).fork()).ldelim(); + return writer; + }; + + /** + * Encodes the specified ResultData message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.IResultData} message ResultData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ResultData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a ResultData message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ResultData.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.documents = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.decode(reader, reader.uint32()); + break; + case 2: + message.counts = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.decode(reader, reader.uint32()); + break; + case 3: + message.sums = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.decode(reader, reader.uint32()); + break; + case 4: + message.averages = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.decode(reader, reader.uint32()); + break; + case 5: + message.ranked = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.decode(reader, reader.uint32()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a ResultData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} ResultData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ResultData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a ResultData message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + ResultData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + var properties = {}; + if (message.documents != null && message.hasOwnProperty("documents")) { + properties.variant = 1; + { var error = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.verify(message.documents); if (error) return "documents." + error; @@ -26733,6 +27006,16 @@ $root.org = (function() { return "averages." + error; } } + if (message.ranked != null && message.hasOwnProperty("ranked")) { + if (properties.variant === 1) + return "variant: multiple values"; + properties.variant = 1; + { + var error = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.verify(message.ranked); + if (error) + return "ranked." + error; + } + } return null; }; @@ -26768,6 +27051,11 @@ $root.org = (function() { throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.averages: object expected"); message.averages = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.fromObject(object.averages); } + if (object.ranked != null) { + if (typeof object.ranked !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.ranked: object expected"); + message.ranked = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.fromObject(object.ranked); + } return message; }; @@ -26804,6 +27092,11 @@ $root.org = (function() { if (options.oneofs) object.variant = "averages"; } + if (message.ranked != null && message.hasOwnProperty("ranked")) { + object.ranked = $root.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject(message.ranked, options); + if (options.oneofs) + object.variant = "ranked"; + } return object; }; diff --git a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js index de7152f9dc9..10f2641e792 100644 --- a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js +++ b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js @@ -171,8 +171,6 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAgg goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.RightCase', null, { proto }); -goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking', null, { proto }); -goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause.TargetCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.VersionCase', null, { proto }); @@ -193,6 +191,9 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocum goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.VariantCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase', null, { proto }); @@ -2257,27 +2258,6 @@ if (goog.DEBUG && !COMPILED) { */ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate'; } -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); -}; -goog.inherits(proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking, jspb.Message); -if (goog.DEBUG && !COMPILED) { - /** - * @public - * @override - */ - proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking'; -} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -2698,6 +2678,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.repeatedFields_, null); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -25454,194 +25476,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.prototype.se - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * Optional fields that are not set will be set to undefined. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * net/proto2/compiler/js/internal/generator.cc#kKeyword. - * @param {boolean=} opt_includeInstance Deprecated. whether to include the - * JSPB instance for transitional soy proto support: - * http://goto/soy-param-migration - * @return {!Object} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.toObject = function(opt_includeInstance) { - return proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Deprecated. Whether to include - * the JSPB instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} msg The msg instance to transform. - * @return {!Object} - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject = function(includeInstance, msg) { - var f, obj = { - kind: jspb.Message.getFieldWithDefault(msg, 1, 0), - n: jspb.Message.getFieldWithDefault(msg, 2, "0") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking; - return proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} */ (reader.readEnum()); - msg.setKind(value); - break; - case 2: - var value = /** @type {string} */ (reader.readUint64String()); - msg.setN(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getKind(); - if (f !== 0.0) { - writer.writeEnum( - 1, - f - ); - } - f = /** @type {string} */ (jspb.Message.getField(message, 2)); - if (f != null) { - writer.writeUint64String( - 2, - f - ); - } -}; - - -/** - * @enum {number} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind = { - MIN: 0, - MAX: 1, - TOP: 2, - BOTTOM: 3 -}; - -/** - * optional Kind kind = 1; - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.getKind = function() { - return /** @type {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); -}; - - -/** - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} value - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.setKind = function(value) { - return jspb.Message.setProto3EnumField(this, 1, value); -}; - - -/** - * optional uint64 n = 2; - * @return {string} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.getN = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); -}; - - -/** - * @param {string} value - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.setN = function(value) { - return jspb.Message.setField(this, 2, value); -}; - - -/** - * Clears the field making it undefined. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.clearN = function() { - return jspb.Message.setField(this, 2, undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.hasN = function() { - return jspb.Message.getField(this, 2) != null; -}; - - - /** * Oneof group definitions for this message. Each group defines the field * numbers belonging to that group. When of these fields' value is set, all @@ -25650,15 +25484,14 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.hasN * @private {!Array>} * @const */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.oneofGroups_ = [[3,4]]; +proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.oneofGroups_ = [[3]]; /** * @enum {number} */ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.RightCase = { RIGHT_NOT_SET: 0, - VALUE: 3, - RANKING: 4 + VALUE: 3 }; /** @@ -25701,8 +25534,7 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.toObject = func var f, obj = { aggregate: (f = msg.getAggregate()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.toObject(includeInstance, f), operator: jspb.Message.getFieldWithDefault(msg, 2, 0), - value: (f = msg.getValue()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.toObject(includeInstance, f), - ranking: (f = msg.getRanking()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject(includeInstance, f) + value: (f = msg.getValue()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.toObject(includeInstance, f) }; if (includeInstance) { @@ -25753,11 +25585,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.deserializeBina reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.deserializeBinaryFromReader); msg.setValue(value); break; - case 4: - var value = new proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking; - reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinaryFromReader); - msg.setRanking(value); - break; default: reader.skipField(); break; @@ -25810,14 +25637,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.serializeBinary proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.serializeBinaryToWriter ); } - f = message.getRanking(); - if (f != null) { - writer.writeMessage( - 4, - f, - proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.serializeBinaryToWriter - ); - } }; @@ -25930,43 +25749,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.hasVa }; -/** - * optional HavingRanking ranking = 4; - * @return {?proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.getRanking = function() { - return /** @type{?proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} */ ( - jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking, 4)); -}; - - -/** - * @param {?proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking|undefined} value - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause} returns this -*/ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.setRanking = function(value) { - return jspb.Message.setOneofWrapperField(this, 4, proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.oneofGroups_[0], value); -}; - - -/** - * Clears the message field making it undefined. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.clearRanking = function() { - return this.setRanking(undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.hasRanking = function() { - return jspb.Message.getField(this, 4) != null; -}; - - /** * Oneof group definitions for this message. Each group defines the field @@ -30824,24 +30606,23 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Aver * @private {!Array>} * @const */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_ = [[1,2,3,4]]; +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_ = [[2,3,4]]; /** * @enum {number} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase = { - VARIANT_NOT_SET: 0, - DOCUMENTS: 1, - COUNTS: 2, - SUMS: 3, - AVERAGES: 4 +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase = { + VALUE_NOT_SET: 0, + COUNT: 2, + SUM: 3, + AVG: 4 }; /** - * @return {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} + * @return {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.getVariantCase = function() { - return /** @type {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} */(jspb.Message.computeOneofCase(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_[0])); +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getValueCase = function() { + return /** @type {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase} */(jspb.Message.computeOneofCase(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0])); }; @@ -30859,8 +30640,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) { * http://goto/soy-param-migration * @return {!Object} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.toObject = function(opt_includeInstance) { - return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject(opt_includeInstance, this); +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject(opt_includeInstance, this); }; @@ -30869,16 +30650,16 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu * @param {boolean|undefined} includeInstance Deprecated. Whether to include * the JSPB instance for transitional soy proto support: * http://goto/soy-param-migration - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The msg instance to transform. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} msg The msg instance to transform. * @return {!Object} * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject = function(includeInstance, msg) { +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject = function(includeInstance, msg) { var f, obj = { - documents: (f = msg.getDocuments()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.toObject(includeInstance, f), - counts: (f = msg.getCounts()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.toObject(includeInstance, f), - sums: (f = msg.getSums()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.toObject(includeInstance, f), - averages: (f = msg.getAverages()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.toObject(includeInstance, f) + key: msg.getKey_asB64(), + count: jspb.Message.getFieldWithDefault(msg, 2, "0"), + sum: jspb.Message.getFieldWithDefault(msg, 3, "0"), + avg: jspb.Message.getFloatingPointFieldWithDefault(msg, 4, 0.0) }; if (includeInstance) { @@ -30892,19 +30673,555 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu /** * Deserializes binary data (in protobuf wire format). * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinary = function(bytes) { +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinary = function(bytes) { var reader = new jspb.BinaryReader(bytes); - var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData; - return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinaryFromReader(msg, reader); + var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry; + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinaryFromReader(msg, reader); }; /** * Deserializes binary data (in protobuf wire format) from the * given reader into the given message object. - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The message object to deserialize into. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setKey(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setCount(value); + break; + case 3: + var value = /** @type {string} */ (reader.readSint64String()); + msg.setSum(value); + break; + case 4: + var value = /** @type {number} */ (reader.readDouble()); + msg.setAvg(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getKey_asU8(); + if (f.length > 0) { + writer.writeBytes( + 1, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeUint64String( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeSint64String( + 3, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeDouble( + 4, + f + ); + } +}; + + +/** + * optional bytes key = 1; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getKey = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * optional bytes key = 1; + * This is a type-conversion wrapper around `getKey()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getKey_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getKey())); +}; + + +/** + * optional bytes key = 1; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getKey()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getKey_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getKey())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setKey = function(value) { + return jspb.Message.setProto3BytesField(this, 1, value); +}; + + +/** + * optional uint64 count = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getCount = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setCount = function(value) { + return jspb.Message.setOneofField(this, 2, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.clearCount = function() { + return jspb.Message.setOneofField(this, 2, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.hasCount = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional sint64 sum = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getSum = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setSum = function(value) { + return jspb.Message.setOneofField(this, 3, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.clearSum = function() { + return jspb.Message.setOneofField(this, 3, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.hasSum = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional double avg = 4; + * @return {number} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getAvg = function() { + return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 4, 0.0)); +}; + + +/** + * @param {number} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setAvg = function(value) { + return jspb.Message.setOneofField(this, 4, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.clearAvg = function() { + return jspb.Message.setOneofField(this, 4, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.hasAvg = function() { + return jspb.Message.getField(this, 4) != null; +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject = function(includeInstance, msg) { + var f, obj = { + entriesList: jspb.Message.toObjectList(msg.getEntriesList(), + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject, includeInstance), + skipped: jspb.Message.getFieldWithDefault(msg, 2, "0") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries; + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinaryFromReader); + msg.addEntries(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setSkipped(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getEntriesList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 1, + f, + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.serializeBinaryToWriter + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeUint64String( + 2, + f + ); + } +}; + + +/** + * repeated RankedEntry entries = 1; + * @return {!Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.getEntriesList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this +*/ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.setEntriesList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 1, value); +}; + + +/** + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry=} opt_value + * @param {number=} opt_index + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.addEntries = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.clearEntriesList = function() { + return this.setEntriesList([]); +}; + + +/** + * optional uint64 skipped = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.getSkipped = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.setSkipped = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.clearSkipped = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.hasSkipped = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_ = [[1,2,3,4,5]]; + +/** + * @enum {number} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase = { + VARIANT_NOT_SET: 0, + DOCUMENTS: 1, + COUNTS: 2, + SUMS: 3, + AVERAGES: 4, + RANKED: 5 +}; + +/** + * @return {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.getVariantCase = function() { + return /** @type {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} */(jspb.Message.computeOneofCase(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_[0])); +}; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject = function(includeInstance, msg) { + var f, obj = { + documents: (f = msg.getDocuments()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.toObject(includeInstance, f), + counts: (f = msg.getCounts()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.toObject(includeInstance, f), + sums: (f = msg.getSums()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.toObject(includeInstance, f), + averages: (f = msg.getAverages()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.toObject(includeInstance, f), + ranked: (f = msg.getRanked()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData; + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The message object to deserialize into. * @param {!jspb.BinaryReader} reader The BinaryReader to use. * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} */ @@ -30935,6 +31252,11 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.deserializeBinaryFromReader); msg.setAverages(value); break; + case 5: + var value = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinaryFromReader); + msg.setRanked(value); + break; default: reader.skipField(); break; @@ -30996,6 +31318,14 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.serializeBinaryToWriter ); } + f = message.getRanked(); + if (f != null) { + writer.writeMessage( + 5, + f, + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.serializeBinaryToWriter + ); + } }; @@ -31147,6 +31477,43 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu }; +/** + * optional RankedEntries ranked = 5; + * @return {?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.getRanked = function() { + return /** @type{?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} */ ( + jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries, 5)); +}; + + +/** + * @param {?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries|undefined} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} returns this +*/ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.setRanked = function(value) { + return jspb.Message.setOneofWrapperField(this, 5, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.clearRanked = function() { + return this.setRanked(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.hasRanked = function() { + return jspb.Message.getField(this, 5) != null; +}; + + /** * optional ResultData data = 1; * @return {?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} diff --git a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h index be03578f98b..90b34897e29 100644 --- a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h +++ b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h @@ -98,7 +98,6 @@ CF_EXTERN_C_BEGIN @class GetDocumentsRequest_GetDocumentsRequestV1_Select; @class GetDocumentsRequest_HavingAggregate; @class GetDocumentsRequest_HavingClause; -@class GetDocumentsRequest_HavingRanking; @class GetDocumentsRequest_OrderClause; @class GetDocumentsRequest_WhereClause; @class GetDocumentsResponse_GetDocumentsResponseV0; @@ -112,6 +111,8 @@ CF_EXTERN_C_BEGIN @class GetDocumentsResponse_GetDocumentsResponseV1_CountEntry; @class GetDocumentsResponse_GetDocumentsResponseV1_CountResults; @class GetDocumentsResponse_GetDocumentsResponseV1_Documents; +@class GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries; +@class GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry; @class GetDocumentsResponse_GetDocumentsResponseV1_ResultData; @class GetDocumentsResponse_GetDocumentsResponseV1_SumEntries; @class GetDocumentsResponse_GetDocumentsResponseV1_SumEntry; @@ -416,29 +417,6 @@ GPBEnumDescriptor *GetDocumentsRequest_HavingAggregate_Function_EnumDescriptor(v **/ BOOL GetDocumentsRequest_HavingAggregate_Function_IsValidValue(int32_t value); -#pragma mark - Enum GetDocumentsRequest_HavingRanking_Kind - -typedef GPB_ENUM(GetDocumentsRequest_HavingRanking_Kind) { - /** - * Value used if any message's field encounters a value that is not defined - * by this enum. The message will also have C functions to get/set the rawValue - * of the field. - **/ - GetDocumentsRequest_HavingRanking_Kind_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, - GetDocumentsRequest_HavingRanking_Kind_Min = 0, - GetDocumentsRequest_HavingRanking_Kind_Max = 1, - GetDocumentsRequest_HavingRanking_Kind_Top = 2, - GetDocumentsRequest_HavingRanking_Kind_Bottom = 3, -}; - -GPBEnumDescriptor *GetDocumentsRequest_HavingRanking_Kind_EnumDescriptor(void); - -/** - * Checks to see if the given value is defined by the enum or was not known at - * the time this source was generated. - **/ -BOOL GetDocumentsRequest_HavingRanking_Kind_IsValidValue(int32_t value); - #pragma mark - Enum GetDocumentsRequest_HavingClause_Operator typedef GPB_ENUM(GetDocumentsRequest_HavingClause_Operator) { @@ -486,11 +464,11 @@ typedef GPB_ENUM(GetDocumentsRequest_GetDocumentsRequestV1_Select_Function) { /** * Per-group MIN / MAX — `SELECT MIN(field) GROUP BY * category` returns the smallest `field` value in each - * category. Semantically distinct from - * `HavingRanking::Min` / `Max` (which are cross-group - * meta-aggregates over group results). MIN/MAX here - * operate over the row values within each group, the - * same way `SUM` and `AVG` do. + * category. These operate over the row values *within* + * each group, the same way `SUM` and `AVG` do; they are + * not a cross-group ranking. To pick out the extreme + * *group*, order by the aggregate instead: + * `ORDER BY ASC|DESC LIMIT 1` (see `order_by`). **/ GetDocumentsRequest_GetDocumentsRequestV1_Select_Function_Min = 4, GetDocumentsRequest_GetDocumentsRequestV1_Select_Function_Max = 5, @@ -2601,9 +2579,13 @@ typedef GPB_ENUM(GetDocumentsRequest_HavingAggregate_FieldNumber) { /** * Per-group aggregate operand for the left side of a - * `HavingClause`. Only the per-group aggregates live here: - * `MIN` / `MAX` / `TOP` / `BOTTOM` are **cross-group** ranking - * primitives and appear on the right side via `HavingRanking`. + * `HavingClause`, and the aggregate-function target of an + * `OrderClause`. Only the per-group aggregates live here — + * `HAVING` is a boolean predicate over one group's own aggregate, + * and nothing on this message reaches across groups. Cross-group + * ranking ("which groups score highest?") is expressed with SQL's + * ordering surface instead: `ORDER BY + * DESC LIMIT n [OFFSET m]`. See `GetDocumentsRequestV1.order_by`. * * **Field semantics by function**: * - `COUNT`: empty `field` means `COUNT(*)` (group cardinality); @@ -2635,95 +2617,56 @@ int32_t GetDocumentsRequest_HavingAggregate_Function_RawValue(GetDocumentsReques **/ void SetGetDocumentsRequest_HavingAggregate_Function_RawValue(GetDocumentsRequest_HavingAggregate *message, int32_t value); -#pragma mark - GetDocumentsRequest_HavingRanking - -typedef GPB_ENUM(GetDocumentsRequest_HavingRanking_FieldNumber) { - GetDocumentsRequest_HavingRanking_FieldNumber_Kind = 1, - GetDocumentsRequest_HavingRanking_FieldNumber_N = 2, -}; - -/** - * Cross-group ranking primitive on the right side of a - * `HavingClause`. The ranking is computed over the set of - * group-aggregate results (one per `GROUP BY` row), so - * `HAVING COUNT(*) EQ MAX` selects groups whose count equals - * the maximum count across all groups, and - * `HAVING COUNT(*) IN TOP(5)` selects groups whose count is - * among the five largest. Concise way to express top-N / - * bottom-N selection without window functions or - * `ORDER BY` + `LIMIT`. - * - * **Operator compatibility**: - * - Scalar operators (`=`, `!=`, `<`, `<=`, `>`, `>=`) work - * with `MIN` / `MAX`. `TOP` / `BOTTOM` with scalar operators - * only make sense when `n=1` (the single largest / smallest); - * evaluation rejects other combinations as ambiguous. - * - `IN` works with `TOP(n)` / `BOTTOM(n)` for set membership. - * - `BETWEEN*` doesn't compose meaningfully with rankings and - * is rejected at evaluation time. - **/ -GPB_FINAL @interface GetDocumentsRequest_HavingRanking : GPBMessage - -@property(nonatomic, readwrite) GetDocumentsRequest_HavingRanking_Kind kind; - -/** - * N-th rank for `TOP` / `BOTTOM` (1-indexed: `n=1` is the - * single largest / smallest). Required for those two kinds; - * must be unset for `MIN` / `MAX`. The wire allows setting - * it on `MIN` / `MAX` for forward compatibility, but - * evaluation rejects it as a malformed ranking. - **/ -@property(nonatomic, readwrite) uint64_t n; - -@property(nonatomic, readwrite) BOOL hasN; -@end - -/** - * Fetches the raw value of a @c GetDocumentsRequest_HavingRanking's @c kind property, even - * if the value was not defined by the enum at the time the code was generated. - **/ -int32_t GetDocumentsRequest_HavingRanking_Kind_RawValue(GetDocumentsRequest_HavingRanking *message); -/** - * Sets the raw value of an @c GetDocumentsRequest_HavingRanking's @c kind property, allowing - * it to be set to a value that was not defined by the enum at the time the code - * was generated. - **/ -void SetGetDocumentsRequest_HavingRanking_Kind_RawValue(GetDocumentsRequest_HavingRanking *message, int32_t value); - #pragma mark - GetDocumentsRequest_HavingClause typedef GPB_ENUM(GetDocumentsRequest_HavingClause_FieldNumber) { GetDocumentsRequest_HavingClause_FieldNumber_Aggregate = 1, GetDocumentsRequest_HavingClause_FieldNumber_Operator_p = 2, GetDocumentsRequest_HavingClause_FieldNumber_Value = 3, - GetDocumentsRequest_HavingClause_FieldNumber_Ranking = 4, }; typedef GPB_ENUM(GetDocumentsRequest_HavingClause_Right_OneOfCase) { GetDocumentsRequest_HavingClause_Right_OneOfCase_GPBUnsetOneOfCase = 0, GetDocumentsRequest_HavingClause_Right_OneOfCase_Value = 3, - GetDocumentsRequest_HavingClause_Right_OneOfCase_Ranking = 4, }; /** - * Single `HAVING ` clause. Multiple + * Single `HAVING ` clause. Multiple * entries in `GetDocumentsRequestV1.having` combine with * implicit AND — same semantics as multiple `where_clauses` * entries. `HAVING COUNT(*) > 5 AND SUM(amount) > 100` is two * `HavingClause` rows, not a tree. * + * **`HAVING` is a boolean per-group predicate and nothing else.** + * Its right operand is always a literal `DocumentFieldValue`; it + * never names another group or the set of groups. Cross-group + * ranking — "the 5 highest-scoring groups" — is `ORDER BY DESC LIMIT 5`, exactly as in SQL, and is + * served by the ranked executor (protocol v14+). An earlier draft + * of this surface carried the ranking on the right of a `HAVING` + * (`HAVING AVG(grade) IN TOP(5)`); that spelling was removed + * before release rather than deprecated, because it invented + * non-SQL grammar for something SQL already expresses. + * + * **`HAVING` cannot yet combine with an aggregate `ORDER BY`.** + * The ranked executor reads a pre-sorted per-axis secondary and + * has no way to drop groups from the middle of that walk, so a + * request carrying both a non-empty `having` and a ranking + * `order_by` is rejected with `Unsupported` rather than served + * with one of the two silently ignored. + * * The operator set mirrors `WhereOperator` minus `STARTS_WITH` * (prefix matching has no natural meaning against a scalar * aggregate result, even a string-typed one). `BETWEEN*` and * `IN` operand semantics match `WhereOperator`: `BETWEEN*` * expects a 2-element `DocumentFieldValue.list` carrying * `[lower, upper]`, and `IN` expects a `list` of candidate - * values (or a ranking set via `right.ranking`). + * values. * - * The `right` oneof carries either a concrete - * `DocumentFieldValue` (literal comparison target) or a - * `HavingRanking` (cross-group reference). Exactly one is set; - * the wire rejects an unset `right`. + * The `right` oneof exists (rather than a bare + * `DocumentFieldValue` field) so the "unset right operand" case + * stays distinguishable from "the literal null value"; the wire + * rejects an unset `right`. **/ GPB_FINAL @interface GetDocumentsRequest_HavingClause : GPBMessage @@ -2737,8 +2680,6 @@ GPB_FINAL @interface GetDocumentsRequest_HavingClause : GPBMessage @property(nonatomic, readwrite, strong, null_resettable) GetDocumentsRequest_DocumentFieldValue *value; -@property(nonatomic, readwrite, strong, null_resettable) GetDocumentsRequest_HavingRanking *ranking; - @end /** @@ -2773,32 +2714,67 @@ typedef GPB_ENUM(GetDocumentsRequest_OrderClause_Target_OneOfCase) { }; /** - * Single `ORDER BY field ` clause. Multi-field + * Single `ORDER BY ` clause. Multi-field * ordering is expressed by repeating this message at the * request level (`repeated OrderClause order_by = 4`), matching * SQL's `ORDER BY a ASC, b DESC` shape. - * Single ORDER BY entry. Multi-entry ordering is expressed by - * repeating this message at the request level. * * The `target` oneof carries either a plain field name * (`ORDER BY field`) or an aggregate function applied to a - * field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`) — the - * latter sorts per-group result rows produced by `GROUP BY`, - * useful with `LIMIT` for top-N / bottom-N selection at the - * routing layer (overlapping `HavingRanking::Top` / `Bottom` - * but more general because the ranking field can be any - * aggregate, not just count). + * field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`). + * + * **Two distinct roles ride the `field` target.** + * + * 1. *Row ordering* — `select = DOCUMENTS`. `field` names a + * document property and the matched rows come back in that + * order. This is v0's behaviour, unchanged. + * + * 2. *Aggregate ordering* — the **ranked** surface (protocol + * v14+). With a `GROUP BY` and a single aggregate `select`, + * exactly one `order_by` clause naming that select's + * aggregate orders the *groups* by their aggregate value and + * routes the request to the ranked executor: * - * **Aggregate target currently rejected** with - * `Unsupported("ORDER BY on aggregate is not yet implemented")`. - * The wire surface is shipped now so callers can encode the - * shape ahead of server support landing. + * ```text + * SELECT AVG(grade) GROUP BY restaurantId + * ORDER BY grade DESC LIMIT 3 -- 3 best restaurants + * SELECT COUNT(*) GROUP BY restaurantId + * ORDER BY $count DESC LIMIT 10 OFFSET 10 -- busiest, page 2 + * ``` + * + * `SUM(f)` / `AVG(f)` are named by `f` — the same property the + * projection aggregates, which is how `ORDER BY avg(grade)` + * reads once `SELECT` has already fixed the function. + * `COUNT(*)` aggregates no property, so it is named by the + * reserved sentinel **`$count`**. The `$` prefix is what keeps + * the sentinel from colliding with a real document property: + * document properties cannot start with `$` (that namespace is + * the system fields' `$id` / `$ownerId` / …), so `$count` can + * never be mistaken for a column. `DESC` is the "top n" + * reading, `ASC` the "bottom n" reading. + * + * An `order_by` naming anything other than the selected + * aggregate — a second clause, the `GROUP BY` property, an + * unrelated field — is rejected rather than normalized: it + * asks for an ordering the ranked secondary cannot produce. + * + * **Aggregate target still rejected** with + * `Unsupported("ORDER BY on aggregate keys is not yet + * implemented")`. It is the *explicit* spelling of role 2 + * (`ORDER BY AVG(grade)` rather than `ORDER BY grade` under a + * `SELECT AVG(grade)`) and is wire-stable so it can start being + * evaluated without another version bump; today the field-target + * spelling above is the one the ranked executor reads. **/ GPB_FINAL @interface GetDocumentsRequest_OrderClause : GPBMessage @property(nonatomic, readonly) GetDocumentsRequest_OrderClause_Target_OneOfCase targetOneOfCase; -/** Plain field name. Today's evaluated form. */ +/** + * Plain field name. Today's evaluated form — a document + * property for row ordering, or the selected aggregate's + * property (`$count` for `COUNT(*)`) for aggregate ordering. + **/ @property(nonatomic, readwrite, copy, null_resettable) NSString *field; /** @@ -2915,12 +2891,18 @@ typedef GPB_ENUM(GetDocumentsRequest_GetDocumentsRequestV1_Start_OneOfCase) { * other shapes return `Unsupported` (see supported-shape table * below). * - * `having` is wire-reserved for a future server capability. Any - * non-empty `having` list currently returns - * `Unsupported("HAVING clause is not yet implemented")` - * regardless of `select` / `group_by`. The wire shape is - * `repeated WhereClause` so when execution lands the surface is - * already typed end-to-end and callers don't need to re-encode. + * **Ranked mode** is served from protocol v14 and is selected by + * `group_by` + a single `order_by` naming the selected aggregate — + * SQL's own top-n spelling, `ORDER BY DESC LIMIT n OFFSET m`. + * It returns `ResultData.ranked`. See `order_by` and the + * supported-shape table below. + * + * `having` is a boolean per-group predicate and is **still** + * `Unsupported` at every protocol version, ranked mode or not + * (`"HAVING clause is not yet implemented"`). It carries no ranking + * spelling: an earlier draft put cross-group ranking on the right of + * a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that grammar was + * removed before release in favour of `ORDER BY` + `LIMIT`. * * **Supported shapes** (everything else rejects with a typed * `QuerySyntaxError::Unsupported` so callers can detect un-wired @@ -2945,8 +2927,13 @@ typedef GPB_ENUM(GetDocumentsRequest_GetDocumentsRequestV1_Start_OneOfCase) { * `select=COUNT, group_by=[a, b]`: * - a is the In field AND b is the range field, in that order → existing compound distinct shape; entries carry both `in_key` (= a's value) and `key` (= b's value). * + * `select=, group_by=[p], order_by=[]` (protocol v14+) — **ranked mode**: + * - exactly one `group_by` property, exactly one `order_by` clause naming the select's aggregate (`f` for `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)`), a `limit` in `1 ..= 100`, an optional `offset`, and no `where` / `having` / `start_at`, on an index declaring the matching `rankedCountable` / `rankedSummable` / `rankedAverageable` axis → ranked executor, answered in `ResultData.ranked`. + * - `DESC` is the "top n" reading (walk the axis from the largest aggregate down), `ASC` the "bottom n" reading. Worked example: `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 1 OFFSET 4` is the 5th-best restaurant. + * * **Rejected shapes** (return `Unsupported`): - * - any non-empty `having` (always — pending future server capability). + * - any non-empty `having`, at every protocol version. + * - at v14+: a ranked-shaped request carrying a `where` clause, a `start_at` / `start_after` cursor, more than one `order_by`, or an `order_by` naming anything but the selected aggregate. * - `select=DOCUMENTS` with non-empty `group_by`. * - `select=COUNT` with `group_by` on a field that is not constrained by an `In` or range where clause. * - `select=COUNT` with `group_by.len() > 2`. @@ -3114,22 +3101,28 @@ GPB_FINAL @interface GetDocumentsRequest_GetDocumentsRequestV1 : GPBMessage @property(nonatomic, readonly) NSUInteger groupByArray_Count; /** - * SQL `HAVING` clauses — aggregate filters that apply to the - * grouped rows produced by `select=COUNT, group_by=[…]`. The - * wire shape is `HavingClause`, not `WhereClause`, because - * HAVING evaluates against per-group aggregates - * (`COUNT`/`SUM`/`AVG`/`MIN`/`MAX`/`TOP`/`BOTTOM`) rather than - * row field values. Multiple entries combine with implicit - * AND. See `HavingClause` / `HavingAggregate` for the - * operator and aggregate-function catalogs. + * SQL `HAVING` clauses — **boolean** aggregate filters that + * apply to the grouped rows produced by `select=, + * group_by=[…]`. The wire shape is `HavingClause`, not + * `WhereClause`, because HAVING evaluates against per-group + * aggregates (`COUNT` / `SUM` / `AVG`) rather than row field + * values. Multiple entries combine with implicit AND. See + * `HavingClause` / `HavingAggregate` for the operator and + * aggregate-function catalogs. * - * **Always rejected when non-empty** today with - * `Unsupported("HAVING clause is not yet implemented")`. The - * wire shape is shipped now so the future server capability - * can land without another version bump — and so callers can - * construct full `HAVING COUNT(*) > 5 AND SUM(amount) > 100` - * requests in their builders even before the server evaluates - * them. + * **Every non-empty `having` is rejected**, at every protocol + * version, with `Unsupported("HAVING clause is not yet + * implemented")`. The wire shape ships ahead of evaluation so + * callers can construct full `HAVING COUNT(*) > 5 AND + * SUM(amount) > 100` requests in their builders, and so the + * capability can land without another version bump. + * + * **`having` does not express ranking.** "The n highest-scoring + * groups" is `ORDER BY DESC LIMIT n` + * (see `order_by`), which *is* served, from protocol v14. An + * earlier draft of this surface spelled it + * `HAVING AVG(grade) IN TOP(5)`; that grammar was removed + * before release rather than deprecated. **/ @property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *havingArray; /** The number of items in @c havingArray without causing the array to be created. */ @@ -3138,13 +3131,28 @@ GPB_FINAL @interface GetDocumentsRequest_GetDocumentsRequestV1 : GPBMessage /** * Row-based pagination offset, on top of the cursor-based * `start_after` / `start_at` pagination. `OFFSET N` skips the - * first `N` matching rows before applying `limit`. Currently - * **always rejected when non-`None`** with - * `Unsupported("OFFSET pagination is not yet implemented")` - * — the wire surface is shipped now so callers can encode it - * ahead of server support landing without another version - * bump. Cursor pagination via `start_after` / `start_at` - * remains the supported way to page through results. + * first `N` result rows before applying `limit`. + * + * **Consumed in ranked mode** (protocol v14+): on a request that + * routes to the ranked executor (`group_by` + a single `order_by` + * naming the selected aggregate), `offset` skips that many ranks + * before the returned page, so `ORDER BY avg(grade) DESC LIMIT 1 + * OFFSET 4` is the 5th-best group. The skip is **count-attested**, + * not walked: grovedb proves it from the counted subtree + * commitments, so the proof stays `O(log n + k)` at any offset and + * the response echoes the attested number in + * `RankedEntries.skipped`. There is deliberately no ceiling — an + * offset of 4 and an offset of four billion cost the same, so + * there is no denial-of-service lever a cap would close. An offset + * past the end of the ranking is a provable answer rather than an + * error: `entries` comes back empty and `skipped` is the ranking's + * whole population. + * + * **Rejected everywhere else** with + * `Unsupported("OFFSET pagination is not yet implemented")` — + * including on every path at protocol v13 and earlier, which has + * no ranked executor. Cursor pagination via `start_after` / + * `start_at` remains the supported way to page through documents. **/ @property(nonatomic, readwrite) uint32_t offset; @@ -3680,6 +3688,162 @@ GPB_FINAL @interface GetDocumentsResponse_GetDocumentsResponseV1_AverageResults **/ void GetDocumentsResponse_GetDocumentsResponseV1_AverageResults_ClearVariantOneOfCase(GetDocumentsResponse_GetDocumentsResponseV1_AverageResults *message); +#pragma mark - GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry + +typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber) { + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Key = 1, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Count = 2, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Sum = 3, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Avg = 4, +}; + +typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_Value_OneOfCase) { + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_Value_OneOfCase_GPBUnsetOneOfCase = 0, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_Value_OneOfCase_Count = 2, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_Value_OneOfCase_Sum = 3, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_Value_OneOfCase_Avg = 4, +}; + +/** + * One group in a ranked (`GROUP BY … ORDER BY LIMIT n`) + * result: the group's index key plus the aggregate it was ranked + * by. + * + * `key` is the raw index-key bytes of the GROUP BY property's + * value — the same bytes that name the group's value tree under + * the index (for a `string` property, its UTF-8 bytes). Clients + * that want the typed value decode it with the document type's + * key deserialization; the wire carries bytes so prover and + * verifier agree without a schema round-trip. + * + * Exactly one `value` variant is set, determined by the + * request's SELECT function: + * * `count` — `SELECT COUNT(*)`, ranked on the index's + * `rankedCountable` axis. + * * `sum` — `SELECT SUM(field)`, `rankedSummable` axis. + * Signed for the same reason `SumEntry.sum` is. + * * `avg` — `SELECT AVG(field)`, + * `rankedAverageable` axis. + **/ +GPB_FINAL @interface GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry : GPBMessage + +@property(nonatomic, readwrite, copy, null_resettable) NSData *key; + +@property(nonatomic, readonly) GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_Value_OneOfCase valueOneOfCase; + +/** + * `jstype = JS_STRING` so JS/Web clients receive a string + * and don't round counts > 2^53−1 to the nearest + * representable Number — same choice as `CountEntry.count`. + **/ +@property(nonatomic, readwrite) uint64_t count; + +/** + * `jstype = JS_STRING` for the same precision reason as + * `SumEntry.sum`. + **/ +@property(nonatomic, readwrite) int64_t sum; + +/** + * The group's average, as a **`double` approximation** of the + * exact value the Avg axis is ordered by. + * + * What grovedb actually commits to and sorts by is an `i128` + * fixed-point integer: `floor(sum * SCALE / count)` with + * euclidean (toward −∞) division, where SCALE is grovedb's + * `AVG_FIXED_POINT_SCALE` (currently 10^19). This field is + * that integer divided by SCALE in `f64`, i.e. + * `fixed_point as f64 / SCALE as f64`. + * + * A `double` is honest here because `RankedEntry` is only ever + * populated on the **no-proof ("quick answer") path**, where + * the client has already chosen to trust the server's reply. + * A proof-verifying client never reads this field: it + * reconstructs each entry from the grovedb proof itself, where + * the exact fixed-point `i128` lives, so nothing about proof + * verification depends on this number's precision. + * + * Precision bound: `f64` carries ~15–16 significant decimal + * digits, so two groups whose exact fixed-point averages differ + * only beyond that can compare equal here. Do not use this + * value for equality checks, tie-breaking, or any + * reconstruction of the committed integer — request the proof + * and read the fixed point from it instead. Entry *order* is + * still exact: the server ranks on the i128 before converting. + * + * SCALE is a grovedb constant, not a wire constant: it moved + * from 10^15 to 10^19 before release. Clients that need it + * (e.g. to go back to fixed point on the proof path) should + * read it from the SDK's re-export (`RANKED_AVG_SCALE`) rather + * than hardcoding the literal. + **/ +@property(nonatomic, readwrite) double avg; + +@end + +/** + * Clears whatever value was set for the oneof 'value'. + **/ +void GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_ClearValueOneOfCase(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry *message); + +#pragma mark - GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries + +typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries_FieldNumber) { + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries_FieldNumber_EntriesArray = 1, + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries_FieldNumber_Skipped = 2, +}; + +/** + * Ranked result entries. **Entry order IS the ranking order** — + * best-first for `ORDER BY DESC`, worst-first for `ASC`. + * Clients must not re-sort; ties (equal aggregates) come back in + * group-key order in the direction of the walk, which is + * descending group-key order for `DESC`. + * + * Fewer than `limit` entries is normal — the index simply has + * fewer groups than requested — and is not an error. + **/ +GPB_FINAL @interface GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries : GPBMessage + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *entriesArray; +/** The number of items in @c entriesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger entriesArray_Count; + +/** + * How many groups were skipped before the first entry — the + * page's **starting rank base**. Entry `i` of `entries` is the + * group at rank `skipped + i` (0-based). + * + * `0` for an `OFFSET 0` (or offset-less) query, which is what + * makes this field additive: a caller that never paginates sees + * the value it would have assumed. For `OFFSET m` it is + * normally `m`, and it is what turns a page back into a + * *ranking* — without it, a caller who asked for + * `ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` receives one + * entry with no way to tell that it really is the 5th-best + * group rather than the best. + * + * **When a requested offset exceeds the population**, `entries` + * is empty and `skipped` is the ranking's attested *total* + * population — a positive, useful answer ("there are only 12 + * groups") rather than a bare empty list. + * + * On the proved path the number is grovedb's cryptographically + * attested count, re-derived by the verifier from the counted + * subtree commitments in the proof bytes rather than trusted + * from this field; a proving client should use the verified + * value. On the unproven read there is nothing to attest and + * grovedb's read API does not report a short walk, so the server + * echoes the requested offset. The two therefore disagree in + * exactly one case — an offset past the end, where the unproven + * read reports the request and the proved one reports the truth. + * Callers who need the population must prove. + **/ +@property(nonatomic, readwrite) uint64_t skipped; + +@property(nonatomic, readwrite) BOOL hasSkipped; +@end + #pragma mark - GetDocumentsResponse_GetDocumentsResponseV1_ResultData typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNumber) { @@ -3687,6 +3851,7 @@ typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNum GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNumber_Counts = 2, GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNumber_Sums = 3, GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNumber_Averages = 4, + GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNumber_Ranked = 5, }; typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_ResultData_Variant_OneOfCase) { @@ -3695,12 +3860,13 @@ typedef GPB_ENUM(GetDocumentsResponse_GetDocumentsResponseV1_ResultData_Variant_ GetDocumentsResponse_GetDocumentsResponseV1_ResultData_Variant_OneOfCase_Counts = 2, GetDocumentsResponse_GetDocumentsResponseV1_ResultData_Variant_OneOfCase_Sums = 3, GetDocumentsResponse_GetDocumentsResponseV1_ResultData_Variant_OneOfCase_Averages = 4, + GetDocumentsResponse_GetDocumentsResponseV1_ResultData_Variant_OneOfCase_Ranked = 5, }; /** * Non-proof result wrapper. The outer `oneof result` switches * between this and `proof`; this inner oneof switches between - * the four non-proof shapes the v1 surface can return. + * the non-proof shapes the v1 surface can return. **/ GPB_FINAL @interface GetDocumentsResponse_GetDocumentsResponseV1_ResultData : GPBMessage @@ -3731,6 +3897,21 @@ GPB_FINAL @interface GetDocumentsResponse_GetDocumentsResponseV1_ResultData : GP **/ @property(nonatomic, readwrite, strong, null_resettable) GetDocumentsResponse_GetDocumentsResponseV1_AverageResults *averages; +/** + * Ranked-aggregate result. Routed when the request pairs a + * single-property `group_by` with a single `order_by` clause + * naming the single aggregate `select` (the `$count` sentinel + * for `COUNT(*)`) and a `limit` — SQL's `ORDER BY + * DESC LIMIT n [OFFSET m]`. Answered from the per-axis + * secondary of an indexed tree, so the index must declare the + * matching `rankedCountable` / `rankedSummable` / + * `rankedAverageable` keyword (meta-schema v3, protocol + * version 14+). Entry order is the ranking order, and + * `skipped` carries the page's starting rank — see + * `RankedEntries`. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries *ranked; + @end /** diff --git a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m index 60bb3a4017a..dd6f5cb79a7 100644 --- a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m +++ b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m @@ -127,7 +127,6 @@ GPBObjCClassDeclaration(GetDocumentsRequest_GetDocumentsRequestV1_Select); GPBObjCClassDeclaration(GetDocumentsRequest_HavingAggregate); GPBObjCClassDeclaration(GetDocumentsRequest_HavingClause); -GPBObjCClassDeclaration(GetDocumentsRequest_HavingRanking); GPBObjCClassDeclaration(GetDocumentsRequest_OrderClause); GPBObjCClassDeclaration(GetDocumentsRequest_WhereClause); GPBObjCClassDeclaration(GetDocumentsResponse); @@ -142,6 +141,8 @@ GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_CountEntry); GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_CountResults); GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_Documents); +GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries); +GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry); GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_ResultData); GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_SumEntries); GPBObjCClassDeclaration(GetDocumentsResponse_GetDocumentsResponseV1_SumEntry); @@ -5566,114 +5567,6 @@ BOOL GetDocumentsRequest_HavingAggregate_Function_IsValidValue(int32_t value__) } } -#pragma mark - GetDocumentsRequest_HavingRanking - -@implementation GetDocumentsRequest_HavingRanking - -@dynamic kind; -@dynamic hasN, n; - -typedef struct GetDocumentsRequest_HavingRanking__storage_ { - uint32_t _has_storage_[1]; - GetDocumentsRequest_HavingRanking_Kind kind; - uint64_t n; -} GetDocumentsRequest_HavingRanking__storage_; - -// This method is threadsafe because it is initially called -// in +initialize for each subclass. -+ (GPBDescriptor *)descriptor { - static GPBDescriptor *descriptor = nil; - if (!descriptor) { - static GPBMessageFieldDescription fields[] = { - { - .name = "kind", - .dataTypeSpecific.enumDescFunc = GetDocumentsRequest_HavingRanking_Kind_EnumDescriptor, - .number = GetDocumentsRequest_HavingRanking_FieldNumber_Kind, - .hasIndex = 0, - .offset = (uint32_t)offsetof(GetDocumentsRequest_HavingRanking__storage_, kind), - .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor | GPBFieldClearHasIvarOnZero), - .dataType = GPBDataTypeEnum, - }, - { - .name = "n", - .dataTypeSpecific.clazz = Nil, - .number = GetDocumentsRequest_HavingRanking_FieldNumber_N, - .hasIndex = 1, - .offset = (uint32_t)offsetof(GetDocumentsRequest_HavingRanking__storage_, n), - .flags = GPBFieldOptional, - .dataType = GPBDataTypeUInt64, - }, - }; - GPBDescriptor *localDescriptor = - [GPBDescriptor allocDescriptorForClass:[GetDocumentsRequest_HavingRanking class] - rootClass:[PlatformRoot class] - file:PlatformRoot_FileDescriptor() - fields:fields - fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) - storageSize:sizeof(GetDocumentsRequest_HavingRanking__storage_) - flags:(GPBDescriptorInitializationFlags)(GPBDescriptorInitializationFlag_UsesClassRefs | GPBDescriptorInitializationFlag_Proto3OptionalKnown)]; - [localDescriptor setupContainingMessageClass:GPBObjCClass(GetDocumentsRequest)]; - #if defined(DEBUG) && DEBUG - NSAssert(descriptor == nil, @"Startup recursed!"); - #endif // DEBUG - descriptor = localDescriptor; - } - return descriptor; -} - -@end - -int32_t GetDocumentsRequest_HavingRanking_Kind_RawValue(GetDocumentsRequest_HavingRanking *message) { - GPBDescriptor *descriptor = [GetDocumentsRequest_HavingRanking descriptor]; - GPBFieldDescriptor *field = [descriptor fieldWithNumber:GetDocumentsRequest_HavingRanking_FieldNumber_Kind]; - return GPBGetMessageRawEnumField(message, field); -} - -void SetGetDocumentsRequest_HavingRanking_Kind_RawValue(GetDocumentsRequest_HavingRanking *message, int32_t value) { - GPBDescriptor *descriptor = [GetDocumentsRequest_HavingRanking descriptor]; - GPBFieldDescriptor *field = [descriptor fieldWithNumber:GetDocumentsRequest_HavingRanking_FieldNumber_Kind]; - GPBSetMessageRawEnumField(message, field, value); -} - -#pragma mark - Enum GetDocumentsRequest_HavingRanking_Kind - -GPBEnumDescriptor *GetDocumentsRequest_HavingRanking_Kind_EnumDescriptor(void) { - static _Atomic(GPBEnumDescriptor*) descriptor = nil; - if (!descriptor) { - static const char *valueNames = - "Min\000Max\000Top\000Bottom\000"; - static const int32_t values[] = { - GetDocumentsRequest_HavingRanking_Kind_Min, - GetDocumentsRequest_HavingRanking_Kind_Max, - GetDocumentsRequest_HavingRanking_Kind_Top, - GetDocumentsRequest_HavingRanking_Kind_Bottom, - }; - GPBEnumDescriptor *worker = - [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GetDocumentsRequest_HavingRanking_Kind) - valueNames:valueNames - values:values - count:(uint32_t)(sizeof(values) / sizeof(int32_t)) - enumVerifier:GetDocumentsRequest_HavingRanking_Kind_IsValidValue]; - GPBEnumDescriptor *expected = nil; - if (!atomic_compare_exchange_strong(&descriptor, &expected, worker)) { - [worker release]; - } - } - return descriptor; -} - -BOOL GetDocumentsRequest_HavingRanking_Kind_IsValidValue(int32_t value__) { - switch (value__) { - case GetDocumentsRequest_HavingRanking_Kind_Min: - case GetDocumentsRequest_HavingRanking_Kind_Max: - case GetDocumentsRequest_HavingRanking_Kind_Top: - case GetDocumentsRequest_HavingRanking_Kind_Bottom: - return YES; - default: - return NO; - } -} - #pragma mark - GetDocumentsRequest_HavingClause @implementation GetDocumentsRequest_HavingClause @@ -5682,14 +5575,12 @@ @implementation GetDocumentsRequest_HavingClause @dynamic hasAggregate, aggregate; @dynamic operator_p; @dynamic value; -@dynamic ranking; typedef struct GetDocumentsRequest_HavingClause__storage_ { uint32_t _has_storage_[2]; GetDocumentsRequest_HavingClause_Operator operator_p; GetDocumentsRequest_HavingAggregate *aggregate; GetDocumentsRequest_DocumentFieldValue *value; - GetDocumentsRequest_HavingRanking *ranking; } GetDocumentsRequest_HavingClause__storage_; // This method is threadsafe because it is initially called @@ -5725,15 +5616,6 @@ + (GPBDescriptor *)descriptor { .flags = GPBFieldOptional, .dataType = GPBDataTypeMessage, }, - { - .name = "ranking", - .dataTypeSpecific.clazz = GPBObjCClass(GetDocumentsRequest_HavingRanking), - .number = GetDocumentsRequest_HavingClause_FieldNumber_Ranking, - .hasIndex = -1, - .offset = (uint32_t)offsetof(GetDocumentsRequest_HavingClause__storage_, ranking), - .flags = GPBFieldOptional, - .dataType = GPBDataTypeMessage, - }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetDocumentsRequest_HavingClause class] @@ -7273,6 +7155,154 @@ void GetDocumentsResponse_GetDocumentsResponseV1_AverageResults_ClearVariantOneO GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; GPBClearOneof(message, oneof); } +#pragma mark - GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry + +@implementation GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry + +@dynamic valueOneOfCase; +@dynamic key; +@dynamic count; +@dynamic sum; +@dynamic avg; + +typedef struct GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_ { + uint32_t _has_storage_[2]; + NSData *key; + uint64_t count; + int64_t sum; + double avg; +} GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "key", + .dataTypeSpecific.clazz = Nil, + .number = GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Key, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_, key), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), + .dataType = GPBDataTypeBytes, + }, + { + .name = "count", + .dataTypeSpecific.clazz = Nil, + .number = GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Count, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_, count), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeUInt64, + }, + { + .name = "sum", + .dataTypeSpecific.clazz = Nil, + .number = GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Sum, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_, sum), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeSInt64, + }, + { + .name = "avg", + .dataTypeSpecific.clazz = Nil, + .number = GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_FieldNumber_Avg, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_, avg), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeDouble, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry class] + rootClass:[PlatformRoot class] + file:PlatformRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry__storage_) + flags:(GPBDescriptorInitializationFlags)(GPBDescriptorInitializationFlag_UsesClassRefs | GPBDescriptorInitializationFlag_Proto3OptionalKnown)]; + static const char *oneofs[] = { + "value", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + [localDescriptor setupContainingMessageClass:GPBObjCClass(GetDocumentsResponse_GetDocumentsResponseV1)]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry_ClearValueOneOfCase(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry *message) { + GPBDescriptor *descriptor = [GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBClearOneof(message, oneof); +} +#pragma mark - GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries + +@implementation GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries + +@dynamic entriesArray, entriesArray_Count; +@dynamic hasSkipped, skipped; + +typedef struct GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *entriesArray; + uint64_t skipped; +} GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "entriesArray", + .dataTypeSpecific.clazz = GPBObjCClass(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntry), + .number = GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries_FieldNumber_EntriesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries__storage_, entriesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "skipped", + .dataTypeSpecific.clazz = Nil, + .number = GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries_FieldNumber_Skipped, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries__storage_, skipped), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeUInt64, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries class] + rootClass:[PlatformRoot class] + file:PlatformRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries__storage_) + flags:(GPBDescriptorInitializationFlags)(GPBDescriptorInitializationFlag_UsesClassRefs | GPBDescriptorInitializationFlag_Proto3OptionalKnown)]; + [localDescriptor setupContainingMessageClass:GPBObjCClass(GetDocumentsResponse_GetDocumentsResponseV1)]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + #pragma mark - GetDocumentsResponse_GetDocumentsResponseV1_ResultData @implementation GetDocumentsResponse_GetDocumentsResponseV1_ResultData @@ -7282,6 +7312,7 @@ @implementation GetDocumentsResponse_GetDocumentsResponseV1_ResultData @dynamic counts; @dynamic sums; @dynamic averages; +@dynamic ranked; typedef struct GetDocumentsResponse_GetDocumentsResponseV1_ResultData__storage_ { uint32_t _has_storage_[2]; @@ -7289,6 +7320,7 @@ @implementation GetDocumentsResponse_GetDocumentsResponseV1_ResultData GetDocumentsResponse_GetDocumentsResponseV1_CountResults *counts; GetDocumentsResponse_GetDocumentsResponseV1_SumResults *sums; GetDocumentsResponse_GetDocumentsResponseV1_AverageResults *averages; + GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries *ranked; } GetDocumentsResponse_GetDocumentsResponseV1_ResultData__storage_; // This method is threadsafe because it is initially called @@ -7333,6 +7365,15 @@ + (GPBDescriptor *)descriptor { .flags = GPBFieldOptional, .dataType = GPBDataTypeMessage, }, + { + .name = "ranked", + .dataTypeSpecific.clazz = GPBObjCClass(GetDocumentsResponse_GetDocumentsResponseV1_RankedEntries), + .number = GetDocumentsResponse_GetDocumentsResponseV1_ResultData_FieldNumber_Ranked, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GetDocumentsResponse_GetDocumentsResponseV1_ResultData__storage_, ranked), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetDocumentsResponse_GetDocumentsResponseV1_ResultData class] diff --git a/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py b/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py index 92539597cd8..004bbbe6f33 100644 --- a/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py +++ b/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py @@ -23,7 +23,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0eplatform.proto\x12\x19org.dash.platform.dapi.v0\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x81\x01\n\x05Proof\x12\x15\n\rgrovedb_proof\x18\x01 \x01(\x0c\x12\x13\n\x0bquorum_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\r\n\x05round\x18\x04 \x01(\r\x12\x15\n\rblock_id_hash\x18\x05 \x01(\x0c\x12\x13\n\x0bquorum_type\x18\x06 \x01(\r\"\x98\x01\n\x10ResponseMetadata\x12\x12\n\x06height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12 \n\x18\x63ore_chain_locked_height\x18\x02 \x01(\r\x12\r\n\x05\x65poch\x18\x03 \x01(\r\x12\x13\n\x07time_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\t\"L\n\x1dStateTransitionBroadcastError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\";\n\x1f\x42roadcastStateTransitionRequest\x12\x18\n\x10state_transition\x18\x01 \x01(\x0c\"\"\n BroadcastStateTransitionResponse\"\xa4\x01\n\x12GetIdentityRequest\x12P\n\x02v0\x18\x01 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetIdentityRequest.GetIdentityRequestV0H\x00\x1a\x31\n\x14GetIdentityRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xc1\x01\n\x17GetIdentityNonceRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityNonceRequest.GetIdentityNonceRequestV0H\x00\x1a?\n\x19GetIdentityNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf6\x01\n\x1fGetIdentityContractNonceRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest.GetIdentityContractNonceRequestV0H\x00\x1a\\\n!GetIdentityContractNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xc0\x01\n\x19GetIdentityBalanceRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetIdentityBalanceRequest.GetIdentityBalanceRequestV0H\x00\x1a\x38\n\x1bGetIdentityBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xec\x01\n$GetIdentityBalanceAndRevisionRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest.GetIdentityBalanceAndRevisionRequestV0H\x00\x1a\x43\n&GetIdentityBalanceAndRevisionRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9e\x02\n\x13GetIdentityResponse\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetIdentityResponse.GetIdentityResponseV0H\x00\x1a\xa7\x01\n\x15GetIdentityResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x02\n\x18GetIdentityNonceResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetIdentityNonceResponse.GetIdentityNonceResponseV0H\x00\x1a\xb6\x01\n\x1aGetIdentityNonceResponseV0\x12\x1c\n\x0eidentity_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe5\x02\n GetIdentityContractNonceResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse.GetIdentityContractNonceResponseV0H\x00\x1a\xc7\x01\n\"GetIdentityContractNonceResponseV0\x12%\n\x17identity_contract_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n\x1aGetIdentityBalanceResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetIdentityBalanceResponse.GetIdentityBalanceResponseV0H\x00\x1a\xb1\x01\n\x1cGetIdentityBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb1\x04\n%GetIdentityBalanceAndRevisionResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0H\x00\x1a\x84\x03\n\'GetIdentityBalanceAndRevisionResponseV0\x12\x9b\x01\n\x14\x62\x61lance_and_revision\x18\x01 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0.BalanceAndRevisionH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x12\x42\x61lanceAndRevision\x12\x13\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x14\n\x08revision\x18\x02 \x01(\x04\x42\x02\x30\x01\x42\x08\n\x06resultB\t\n\x07version\"\xd1\x01\n\x0eKeyRequestType\x12\x36\n\x08\x61ll_keys\x18\x01 \x01(\x0b\x32\".org.dash.platform.dapi.v0.AllKeysH\x00\x12@\n\rspecific_keys\x18\x02 \x01(\x0b\x32\'.org.dash.platform.dapi.v0.SpecificKeysH\x00\x12:\n\nsearch_key\x18\x03 \x01(\x0b\x32$.org.dash.platform.dapi.v0.SearchKeyH\x00\x42\t\n\x07request\"\t\n\x07\x41llKeys\"\x1f\n\x0cSpecificKeys\x12\x0f\n\x07key_ids\x18\x01 \x03(\r\"\xb6\x01\n\tSearchKey\x12I\n\x0bpurpose_map\x18\x01 \x03(\x0b\x32\x34.org.dash.platform.dapi.v0.SearchKey.PurposeMapEntry\x1a^\n\x0fPurposeMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.org.dash.platform.dapi.v0.SecurityLevelMap:\x02\x38\x01\"\xbf\x02\n\x10SecurityLevelMap\x12]\n\x12security_level_map\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.SecurityLevelMap.SecurityLevelMapEntry\x1aw\n\x15SecurityLevelMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12M\n\x05value\x18\x02 \x01(\x0e\x32>.org.dash.platform.dapi.v0.SecurityLevelMap.KeyKindRequestType:\x02\x38\x01\"S\n\x12KeyKindRequestType\x12\x1f\n\x1b\x43URRENT_KEY_OF_KIND_REQUEST\x10\x00\x12\x1c\n\x18\x41LL_KEYS_OF_KIND_REQUEST\x10\x01\"\xda\x02\n\x16GetIdentityKeysRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetIdentityKeysRequest.GetIdentityKeysRequestV0H\x00\x1a\xda\x01\n\x18GetIdentityKeysRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12?\n\x0crequest_type\x18\x02 \x01(\x0b\x32).org.dash.platform.dapi.v0.KeyRequestType\x12+\n\x05limit\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\x99\x03\n\x17GetIdentityKeysResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0H\x00\x1a\x96\x02\n\x19GetIdentityKeysResponseV0\x12\x61\n\x04keys\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0.KeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x04Keys\x12\x12\n\nkeys_bytes\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xef\x02\n GetIdentitiesContractKeysRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest.GetIdentitiesContractKeysRequestV0H\x00\x1a\xd1\x01\n\"GetIdentitiesContractKeysRequestV0\x12\x16\n\x0eidentities_ids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\x1f\n\x12\x64ocument_type_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x37\n\x08purposes\x18\x04 \x03(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x15\n\x13_document_type_nameB\t\n\x07version\"\xdf\x06\n!GetIdentitiesContractKeysResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0H\x00\x1a\xbe\x05\n#GetIdentitiesContractKeysResponseV0\x12\x8a\x01\n\x0fidentities_keys\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentitiesKeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aY\n\x0bPurposeKeys\x12\x36\n\x07purpose\x18\x01 \x01(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\x12\n\nkeys_bytes\x18\x02 \x03(\x0c\x1a\x9f\x01\n\x0cIdentityKeys\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12z\n\x04keys\x18\x02 \x03(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.PurposeKeys\x1a\x90\x01\n\x0eIdentitiesKeys\x12~\n\x07\x65ntries\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentityKeysB\x08\n\x06resultB\t\n\x07version\"\xa4\x02\n*GetEvonodesProposedEpochBlocksByIdsRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest.GetEvonodesProposedEpochBlocksByIdsRequestV0H\x00\x1ah\n,GetEvonodesProposedEpochBlocksByIdsRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0b\n\x03ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x08\n\x06_epochB\t\n\x07version\"\x92\x06\n&GetEvonodesProposedEpochBlocksResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0H\x00\x1a\xe2\x04\n(GetEvonodesProposedEpochBlocksResponseV0\x12\xb1\x01\n#evonodes_proposed_block_counts_info\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodesProposedBlocksH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x15\x45vonodeProposedBlocks\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01\x1a\xc4\x01\n\x16\x45vonodesProposedBlocks\x12\xa9\x01\n\x1e\x65vonodes_proposed_block_counts\x18\x01 \x03(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodeProposedBlocksB\x08\n\x06resultB\t\n\x07version\"\xf2\x02\n,GetEvonodesProposedEpochBlocksByRangeRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest.GetEvonodesProposedEpochBlocksByRangeRequestV0H\x00\x1a\xaf\x01\n.GetEvonodesProposedEpochBlocksByRangeRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x02\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x04 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_epochB\x08\n\x06_limitB\t\n\x07version\"\xcd\x01\n\x1cGetIdentitiesBalancesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest.GetIdentitiesBalancesRequestV0H\x00\x1a<\n\x1eGetIdentitiesBalancesRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x05\n\x1dGetIdentitiesBalancesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0H\x00\x1a\x8a\x04\n\x1fGetIdentitiesBalancesResponseV0\x12\x8a\x01\n\x13identities_balances\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentitiesBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aL\n\x0fIdentityBalance\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x18\n\x07\x62\x61lance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x8f\x01\n\x12IdentitiesBalances\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentityBalanceB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x16GetDataContractRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetDataContractRequest.GetDataContractRequestV0H\x00\x1a\x35\n\x18GetDataContractRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xb3\x02\n\x17GetDataContractResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractResponse.GetDataContractResponseV0H\x00\x1a\xb0\x01\n\x19GetDataContractResponseV0\x12\x17\n\rdata_contract\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb9\x01\n\x17GetDataContractsRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractsRequest.GetDataContractsRequestV0H\x00\x1a\x37\n\x19GetDataContractsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xcf\x04\n\x18GetDataContractsResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetDataContractsResponse.GetDataContractsResponseV0H\x00\x1a[\n\x11\x44\x61taContractEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x32\n\rdata_contract\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x1au\n\rDataContracts\x12\x64\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32\x45.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractEntry\x1a\xf5\x01\n\x1aGetDataContractsResponseV0\x12[\n\x0e\x64\x61ta_contracts\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc5\x02\n\x1dGetDataContractHistoryRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetDataContractHistoryRequest.GetDataContractHistoryRequestV0H\x00\x1a\xb0\x01\n\x1fGetDataContractHistoryRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xb2\x05\n\x1eGetDataContractHistoryResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0H\x00\x1a\x9a\x04\n GetDataContractHistoryResponseV0\x12\x8f\x01\n\x15\x64\x61ta_contract_history\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a;\n\x18\x44\x61taContractHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\xaa\x01\n\x13\x44\x61taContractHistory\x12\x92\x01\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32s.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xdb\x17\n\x13GetDocumentsRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV0H\x00\x12R\n\x02v1\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1H\x00\x1a\xfe\x02\n\x12\x44ocumentFieldValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x19\n\x0bint64_value\x18\x02 \x01(\x12\x42\x02\x30\x01H\x00\x12\x1a\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12\x0e\n\x04text\x18\x05 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x12[\n\x04list\x18\x07 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.ValueListH\x00\x12\x14\n\nnull_value\x18\x08 \x01(\x08H\x00\x1a^\n\tValueList\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueB\t\n\x07variant\x1a\xbe\x01\n\x0bWhereClause\x12\r\n\x05\x66ield\x18\x01 \x01(\t\x12N\n\x08operator\x18\x02 \x01(\x0e\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator\x12P\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x1a\xa4\x01\n\x0fHavingAggregate\x12Y\n\x08\x66unction\x18\x01 \x01(\x0e\x32G.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"\'\n\x08\x46unction\x12\t\n\x05\x43OUNT\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x07\n\x03\x41VG\x10\x02\x1a\xa9\x01\n\rHavingRanking\x12O\n\x04kind\x18\x01 \x01(\x0e\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind\x12\x12\n\x01n\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\"-\n\x04Kind\x12\x07\n\x03MIN\x10\x00\x12\x07\n\x03MAX\x10\x01\x12\x07\n\x03TOP\x10\x02\x12\n\n\x06\x42OTTOM\x10\x03\x42\x04\n\x02_n\x1a\xca\x04\n\x0cHavingClause\x12Q\n\taggregate\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate\x12V\n\x08operator\x18\x02 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator\x12R\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueH\x00\x12O\n\x07ranking\x18\x04 \x01(\x0b\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRankingH\x00\"\xe0\x01\n\x08Operator\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x05\x12\x0b\n\x07\x42\x45TWEEN\x10\x06\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x07\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x08\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\t\x12\x06\n\x02IN\x10\nB\x07\n\x05right\x1a\x90\x01\n\x0bOrderClause\x12\x0f\n\x05\x66ield\x18\x01 \x01(\tH\x00\x12S\n\taggregate\x18\x03 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregateH\x00\x12\x11\n\tascending\x18\x02 \x01(\x08\x42\x08\n\x06target\x1a\xbb\x01\n\x15GetDocumentsRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12\r\n\x05where\x18\x03 \x01(\x0c\x12\x10\n\x08order_by\x18\x04 \x01(\x0c\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x42\x07\n\x05start\x1a\xf3\x05\n\x15GetDocumentsRequestV1\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x12\\\n\x07selects\x18\t \x03(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select\x12\x10\n\x08group_by\x18\n \x03(\t\x12K\n\x06having\x18\x0b \x03(\x0b\x32;.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause\x12\x13\n\x06offset\x18\x0c \x01(\rH\x02\x88\x01\x01\x1a\xc9\x01\n\x06Select\x12\x66\n\x08\x66unction\x18\x01 \x01(\x0e\x32T.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"H\n\x08\x46unction\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x12\x07\n\x03SUM\x10\x02\x12\x07\n\x03\x41VG\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x42\x07\n\x05startB\x08\n\x06_limitB\t\n\x07_offset\"\xe7\x01\n\rWhereOperator\x12\t\n\x05\x45QUAL\x10\x00\x12\x10\n\x0cGREATER_THAN\x10\x01\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x02\x12\r\n\tLESS_THAN\x10\x03\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x04\x12\x0b\n\x07\x42\x45TWEEN\x10\x05\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x06\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x07\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\x08\x12\x06\n\x02IN\x10\t\x12\x0f\n\x0bSTARTS_WITH\x10\nB\t\n\x07version\"\x86\x13\n\x14GetDocumentsResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0H\x00\x12T\n\x02v1\x18\x02 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1H\x00\x1a\x9b\x02\n\x16GetDocumentsResponseV0\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0.DocumentsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x42\x08\n\x06result\x1a\x98\x0f\n\x16GetDocumentsResponseV1\x12\x61\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x1aL\n\nCountEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07_in_key\x1ar\n\x0c\x43ountEntries\x12\x62\n\x07\x65ntries\x18\x01 \x03(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntry\x1a\xa0\x01\n\x0c\x43ountResults\x12\x1d\n\x0f\x61ggregate_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x66\n\x07\x65ntries\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x42\t\n\x07variant\x1aH\n\x08SumEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x0f\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1an\n\nSumEntries\x12`\n\x07\x65ntries\x18\x01 \x03(\x0b\x32O.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntry\x1a\x9a\x01\n\nSumResults\x12\x1b\n\raggregate_sum\x18\x01 \x01(\x12\x42\x02\x30\x01H\x00\x12\x64\n\x07\x65ntries\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntriesH\x00\x42\t\n\x07variant\x1a_\n\x0c\x41verageEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x04 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1av\n\x0e\x41verageEntries\x12\x64\n\x07\x65ntries\x18\x01 \x03(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntry\x1a\x36\n\x10\x41verageAggregate\x12\x11\n\x05\x63ount\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x02 \x01(\x12\x42\x02\x30\x01\x1a\xfb\x01\n\x0e\x41verageResults\x12t\n\x11\x61ggregate_average\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregateH\x00\x12h\n\x07\x65ntries\x18\x02 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntriesH\x00\x42\t\n\x07variant\x1a\xb3\x03\n\nResultData\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResultsH\x00\x12\x61\n\x04sums\x18\x03 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResultsH\x00\x12i\n\x08\x61verages\x18\x04 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResultsH\x00\x42\t\n\x07variantB\x08\n\x06resultB\t\n\x07version\"\xf4\x02\n\x19GetDocumentHistoryRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentHistoryRequest.GetDocumentHistoryRequestV0H\x00\x1a\xeb\x01\n\x1bGetDocumentHistoryRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x03 \x01(\x0c\x12+\n\x05limit\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x07 \x01(\x08\x42\t\n\x07version\"\xf7\x04\n\x1aGetDocumentHistoryResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0H\x00\x1a\xeb\x03\n\x1cGetDocumentHistoryResponseV0\x12~\n\x10\x64ocument_history\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x37\n\x14\x44ocumentHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\x95\x01\n\x0f\x44ocumentHistory\x12\x81\x01\n\x10\x64ocument_entries\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xed\x01\n!GetIdentityByPublicKeyHashRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest.GetIdentityByPublicKeyHashRequestV0H\x00\x1aM\n#GetIdentityByPublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xda\x02\n\"GetIdentityByPublicKeyHashResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse.GetIdentityByPublicKeyHashResponseV0H\x00\x1a\xb6\x01\n$GetIdentityByPublicKeyHashResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n*GetIdentityByNonUniquePublicKeyHashRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest.GetIdentityByNonUniquePublicKeyHashRequestV0H\x00\x1a\x80\x01\n,GetIdentityByNonUniquePublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\x18\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x0e\n\x0c_start_afterB\t\n\x07version\"\xd6\x06\n+GetIdentityByNonUniquePublicKeyHashResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0H\x00\x1a\x96\x05\n-GetIdentityByNonUniquePublicKeyHashResponseV0\x12\x9a\x01\n\x08identity\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityResponseH\x00\x12\x9d\x01\n\x05proof\x18\x02 \x01(\x0b\x32\x8b\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityProvedResponseH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x10IdentityResponse\x12\x15\n\x08identity\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_identity\x1a\xa6\x01\n\x16IdentityProvedResponse\x12P\n&grovedb_identity_public_key_hash_proof\x18\x01 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12!\n\x14identity_proof_bytes\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x17\n\x15_identity_proof_bytesB\x08\n\x06resultB\t\n\x07version\"\xfb\x01\n#WaitForStateTransitionResultRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest.WaitForStateTransitionResultRequestV0H\x00\x1aU\n%WaitForStateTransitionResultRequestV0\x12\x1d\n\x15state_transition_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x99\x03\n$WaitForStateTransitionResultResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.WaitForStateTransitionResultResponseV0H\x00\x1a\xef\x01\n&WaitForStateTransitionResultResponseV0\x12I\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x38.org.dash.platform.dapi.v0.StateTransitionBroadcastErrorH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x19GetConsensusParamsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetConsensusParamsRequest.GetConsensusParamsRequestV0H\x00\x1a<\n\x1bGetConsensusParamsRequestV0\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9c\x04\n\x1aGetConsensusParamsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetConsensusParamsResponse.GetConsensusParamsResponseV0H\x00\x1aP\n\x14\x43onsensusParamsBlock\x12\x11\n\tmax_bytes\x18\x01 \x01(\t\x12\x0f\n\x07max_gas\x18\x02 \x01(\t\x12\x14\n\x0ctime_iota_ms\x18\x03 \x01(\t\x1a\x62\n\x17\x43onsensusParamsEvidence\x12\x1a\n\x12max_age_num_blocks\x18\x01 \x01(\t\x12\x18\n\x10max_age_duration\x18\x02 \x01(\t\x12\x11\n\tmax_bytes\x18\x03 \x01(\t\x1a\xda\x01\n\x1cGetConsensusParamsResponseV0\x12Y\n\x05\x62lock\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsBlock\x12_\n\x08\x65vidence\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsEvidenceB\t\n\x07version\"\xe4\x01\n%GetProtocolVersionUpgradeStateRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest.GetProtocolVersionUpgradeStateRequestV0H\x00\x1a\x38\n\'GetProtocolVersionUpgradeStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb5\x05\n&GetProtocolVersionUpgradeStateResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0H\x00\x1a\x85\x04\n(GetProtocolVersionUpgradeStateResponseV0\x12\x87\x01\n\x08versions\x18\x01 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x96\x01\n\x08Versions\x12\x89\x01\n\x08versions\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionEntry\x1a:\n\x0cVersionEntry\x12\x16\n\x0eversion_number\x18\x01 \x01(\r\x12\x12\n\nvote_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xa3\x02\n*GetProtocolVersionUpgradeVoteStatusRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest.GetProtocolVersionUpgradeVoteStatusRequestV0H\x00\x1ag\n,GetProtocolVersionUpgradeVoteStatusRequestV0\x12\x19\n\x11start_pro_tx_hash\x18\x01 \x01(\x0c\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xef\x05\n+GetProtocolVersionUpgradeVoteStatusResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0H\x00\x1a\xaf\x04\n-GetProtocolVersionUpgradeVoteStatusResponseV0\x12\x98\x01\n\x08versions\x18\x01 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xaf\x01\n\x0eVersionSignals\x12\x9c\x01\n\x0fversion_signals\x18\x01 \x03(\x0b\x32\x82\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignal\x1a\x35\n\rVersionSignal\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07version\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xf5\x01\n\x14GetEpochsInfoRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetEpochsInfoRequest.GetEpochsInfoRequestV0H\x00\x1a|\n\x16GetEpochsInfoRequestV0\x12\x31\n\x0bstart_epoch\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x11\n\tascending\x18\x03 \x01(\x08\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\x99\x05\n\x15GetEpochsInfoResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0H\x00\x1a\x9c\x04\n\x17GetEpochsInfoResponseV0\x12\x65\n\x06\x65pochs\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1au\n\nEpochInfos\x12g\n\x0b\x65poch_infos\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfo\x1a\xa6\x01\n\tEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x16\n\nstart_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xbf\x02\n\x1dGetFinalizedEpochInfosRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest.GetFinalizedEpochInfosRequestV0H\x00\x1a\xaa\x01\n\x1fGetFinalizedEpochInfosRequestV0\x12\x19\n\x11start_epoch_index\x18\x01 \x01(\r\x12\"\n\x1astart_epoch_index_included\x18\x02 \x01(\x08\x12\x17\n\x0f\x65nd_epoch_index\x18\x03 \x01(\r\x12 \n\x18\x65nd_epoch_index_included\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xbd\t\n\x1eGetFinalizedEpochInfosResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0H\x00\x1a\xa5\x08\n GetFinalizedEpochInfosResponseV0\x12\x80\x01\n\x06\x65pochs\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xa4\x01\n\x13\x46inalizedEpochInfos\x12\x8c\x01\n\x15\x66inalized_epoch_infos\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfo\x1a\x9f\x04\n\x12\x46inalizedEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x1c\n\x10\x66irst_block_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\x12!\n\x15total_blocks_in_epoch\x18\x07 \x01(\x04\x42\x02\x30\x01\x12*\n\"next_epoch_start_core_block_height\x18\x08 \x01(\r\x12!\n\x15total_processing_fees\x18\t \x01(\x04\x42\x02\x30\x01\x12*\n\x1etotal_distributed_storage_fees\x18\n \x01(\x04\x42\x02\x30\x01\x12&\n\x1atotal_created_storage_fees\x18\x0b \x01(\x04\x42\x02\x30\x01\x12\x1e\n\x12\x63ore_block_rewards\x18\x0c \x01(\x04\x42\x02\x30\x01\x12\x81\x01\n\x0f\x62lock_proposers\x18\r \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.BlockProposer\x1a\x39\n\rBlockProposer\x12\x13\n\x0bproposer_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x62lock_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xde\x04\n\x1cGetContestedResourcesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0H\x00\x1a\xcc\x03\n\x1eGetContestedResourcesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x1a\n\x12start_index_values\x18\x04 \x03(\x0c\x12\x18\n\x10\x65nd_index_values\x18\x05 \x03(\x0c\x12\x89\x01\n\x13start_at_value_info\x18\x06 \x01(\x0b\x32g.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0.StartAtValueInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1a\x45\n\x10StartAtValueInfo\x12\x13\n\x0bstart_value\x18\x01 \x01(\x0c\x12\x1c\n\x14start_value_included\x18\x02 \x01(\x08\x42\x16\n\x14_start_at_value_infoB\x08\n\x06_countB\t\n\x07version\"\x88\x04\n\x1dGetContestedResourcesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0H\x00\x1a\xf3\x02\n\x1fGetContestedResourcesResponseV0\x12\x95\x01\n\x19\x63ontested_resource_values\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0.ContestedResourceValuesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a<\n\x17\x43ontestedResourceValues\x12!\n\x19\x63ontested_resource_values\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x05\n\x1cGetVotePollsByEndDateRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0H\x00\x1a\xc0\x04\n\x1eGetVotePollsByEndDateRequestV0\x12\x84\x01\n\x0fstart_time_info\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.StartAtTimeInfoH\x00\x88\x01\x01\x12\x80\x01\n\rend_time_info\x18\x02 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.EndAtTimeInfoH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x13\n\x06offset\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x11\n\tascending\x18\x05 \x01(\x08\x12\r\n\x05prove\x18\x06 \x01(\x08\x1aI\n\x0fStartAtTimeInfo\x12\x19\n\rstart_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13start_time_included\x18\x02 \x01(\x08\x1a\x43\n\rEndAtTimeInfo\x12\x17\n\x0b\x65nd_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x65nd_time_included\x18\x02 \x01(\x08\x42\x12\n\x10_start_time_infoB\x10\n\x0e_end_time_infoB\x08\n\x06_limitB\t\n\x07_offsetB\t\n\x07version\"\x83\x06\n\x1dGetVotePollsByEndDateResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0H\x00\x1a\xee\x04\n\x1fGetVotePollsByEndDateResponseV0\x12\x9c\x01\n\x18vote_polls_by_timestamps\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestampsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aV\n\x1eSerializedVotePollsByTimestamp\x12\x15\n\ttimestamp\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x15serialized_vote_polls\x18\x02 \x03(\x0c\x1a\xd7\x01\n\x1fSerializedVotePollsByTimestamps\x12\x99\x01\n\x18vote_polls_by_timestamps\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestamp\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xff\x06\n$GetContestedResourceVoteStateRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0H\x00\x1a\xd5\x05\n&GetContestedResourceVoteStateRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x86\x01\n\x0bresult_type\x18\x05 \x01(\x0e\x32q.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.ResultType\x12\x36\n.allow_include_locked_and_abstaining_vote_tally\x18\x06 \x01(\x08\x12\xa3\x01\n\x18start_at_identifier_info\x18\x07 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x08 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\"I\n\nResultType\x12\r\n\tDOCUMENTS\x10\x00\x12\x0e\n\nVOTE_TALLY\x10\x01\x12\x1c\n\x18\x44OCUMENTS_AND_VOTE_TALLY\x10\x02\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\x94\x0c\n%GetContestedResourceVoteStateResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0H\x00\x1a\xe7\n\n\'GetContestedResourceVoteStateResponseV0\x12\xae\x01\n\x1d\x63ontested_resource_contenders\x18\x01 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.ContestedResourceContendersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xda\x03\n\x10\x46inishedVoteInfo\x12\xad\x01\n\x15\x66inished_vote_outcome\x18\x01 \x01(\x0e\x32\x8d\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfo.FinishedVoteOutcome\x12\x1f\n\x12won_by_identity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12$\n\x18\x66inished_at_block_height\x18\x03 \x01(\x04\x42\x02\x30\x01\x12%\n\x1d\x66inished_at_core_block_height\x18\x04 \x01(\r\x12%\n\x19\x66inished_at_block_time_ms\x18\x05 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x66inished_at_epoch\x18\x06 \x01(\r\"O\n\x13\x46inishedVoteOutcome\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x16\n\x12NO_PREVIOUS_WINNER\x10\x02\x42\x15\n\x13_won_by_identity_id\x1a\xc4\x03\n\x1b\x43ontestedResourceContenders\x12\x86\x01\n\ncontenders\x18\x01 \x03(\x0b\x32r.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.Contender\x12\x1f\n\x12\x61\x62stain_vote_tally\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0flock_vote_tally\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x9a\x01\n\x12\x66inished_vote_info\x18\x04 \x01(\x0b\x32y.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfoH\x02\x88\x01\x01\x42\x15\n\x13_abstain_vote_tallyB\x12\n\x10_lock_vote_tallyB\x15\n\x13_finished_vote_info\x1ak\n\tContender\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x17\n\nvote_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08\x64ocument\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\r\n\x0b_vote_countB\x0b\n\t_documentB\x08\n\x06resultB\t\n\x07version\"\xd5\x05\n,GetContestedResourceVotersForIdentityRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0H\x00\x1a\x92\x04\n.GetContestedResourceVotersForIdentityRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x15\n\rcontestant_id\x18\x05 \x01(\x0c\x12\xb4\x01\n\x18start_at_identifier_info\x18\x06 \x01(\x0b\x32\x8c\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\xf1\x04\n-GetContestedResourceVotersForIdentityResponse\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0H\x00\x1a\xab\x03\n/GetContestedResourceVotersForIdentityResponseV0\x12\xb6\x01\n\x19\x63ontested_resource_voters\x18\x01 \x01(\x0b\x32\x90\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0.ContestedResourceVotersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x43\n\x17\x43ontestedResourceVoters\x12\x0e\n\x06voters\x18\x01 \x03(\x0c\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xad\x05\n(GetContestedResourceIdentityVotesRequest\x12|\n\x02v0\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0H\x00\x1a\xf7\x03\n*GetContestedResourceIdentityVotesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0forder_ascending\x18\x04 \x01(\x08\x12\xae\x01\n\x1astart_at_vote_poll_id_info\x18\x05 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0.StartAtVotePollIdInfoH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x1a\x61\n\x15StartAtVotePollIdInfo\x12 \n\x18start_at_poll_identifier\x18\x01 \x01(\x0c\x12&\n\x1estart_poll_identifier_included\x18\x02 \x01(\x08\x42\x1d\n\x1b_start_at_vote_poll_id_infoB\t\n\x07version\"\xc8\n\n)GetContestedResourceIdentityVotesResponse\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0H\x00\x1a\x8f\t\n+GetContestedResourceIdentityVotesResponseV0\x12\xa1\x01\n\x05votes\x18\x01 \x01(\x0b\x32\x8f\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xf7\x01\n\x1e\x43ontestedResourceIdentityVotes\x12\xba\x01\n!contested_resource_identity_votes\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVote\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x1a\xad\x02\n\x12ResourceVoteChoice\x12\xad\x01\n\x10vote_choice_type\x18\x01 \x01(\x0e\x32\x92\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoice.VoteChoiceType\x12\x18\n\x0bidentity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\"=\n\x0eVoteChoiceType\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\x0b\n\x07\x41\x42STAIN\x10\x01\x12\x08\n\x04LOCK\x10\x02\x42\x0e\n\x0c_identity_id\x1a\x95\x02\n\x1d\x43ontestedResourceIdentityVote\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\'\n\x1fserialized_index_storage_values\x18\x03 \x03(\x0c\x12\x99\x01\n\x0bvote_choice\x18\x04 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoiceB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n%GetPrefundedSpecializedBalanceRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest.GetPrefundedSpecializedBalanceRequestV0H\x00\x1a\x44\n\'GetPrefundedSpecializedBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xed\x02\n&GetPrefundedSpecializedBalanceResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse.GetPrefundedSpecializedBalanceResponseV0H\x00\x1a\xbd\x01\n(GetPrefundedSpecializedBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd0\x01\n GetTotalCreditsInPlatformRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest.GetTotalCreditsInPlatformRequestV0H\x00\x1a\x33\n\"GetTotalCreditsInPlatformRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd9\x02\n!GetTotalCreditsInPlatformResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse.GetTotalCreditsInPlatformResponseV0H\x00\x1a\xb8\x01\n#GetTotalCreditsInPlatformResponseV0\x12\x15\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x16GetPathElementsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetPathElementsRequest.GetPathElementsRequestV0H\x00\x1a\x45\n\x18GetPathElementsRequestV0\x12\x0c\n\x04path\x18\x01 \x03(\x0c\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xa3\x03\n\x17GetPathElementsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0H\x00\x1a\xa0\x02\n\x19GetPathElementsResponseV0\x12i\n\x08\x65lements\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0.ElementsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1c\n\x08\x45lements\x12\x10\n\x08\x65lements\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\x81\x01\n\x10GetStatusRequest\x12L\n\x02v0\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetStatusRequest.GetStatusRequestV0H\x00\x1a\x14\n\x12GetStatusRequestV0B\t\n\x07version\"\xe4\x10\n\x11GetStatusResponse\x12N\n\x02v0\x18\x01 \x01(\x0b\x32@.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0H\x00\x1a\xf3\x0f\n\x13GetStatusResponseV0\x12Y\n\x07version\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version\x12S\n\x04node\x18\x02 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Node\x12U\n\x05\x63hain\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Chain\x12Y\n\x07network\x18\x04 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Network\x12^\n\nstate_sync\x18\x05 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.StateSync\x12S\n\x04time\x18\x06 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Time\x1a\x82\x05\n\x07Version\x12\x63\n\x08software\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Software\x12\x63\n\x08protocol\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol\x1a^\n\x08Software\x12\x0c\n\x04\x64\x61pi\x18\x01 \x01(\t\x12\x12\n\x05\x64rive\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ntenderdash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x08\n\x06_driveB\r\n\x0b_tenderdash\x1a\xcc\x02\n\x08Protocol\x12p\n\ntenderdash\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Tenderdash\x12\x66\n\x05\x64rive\x18\x02 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Drive\x1a(\n\nTenderdash\x12\x0b\n\x03p2p\x18\x01 \x01(\r\x12\r\n\x05\x62lock\x18\x02 \x01(\r\x1a<\n\x05\x44rive\x12\x0e\n\x06latest\x18\x03 \x01(\r\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\r\x12\x12\n\nnext_epoch\x18\x05 \x01(\r\x1a\x7f\n\x04Time\x12\x11\n\x05local\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x05\x62lock\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12\x18\n\x07genesis\x18\x03 \x01(\x04\x42\x02\x30\x01H\x01\x88\x01\x01\x12\x12\n\x05\x65poch\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_blockB\n\n\x08_genesisB\x08\n\x06_epoch\x1a<\n\x04Node\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x18\n\x0bpro_tx_hash\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_pro_tx_hash\x1a\xb3\x02\n\x05\x43hain\x12\x13\n\x0b\x63\x61tching_up\x18\x01 \x01(\x08\x12\x19\n\x11latest_block_hash\x18\x02 \x01(\x0c\x12\x17\n\x0flatest_app_hash\x18\x03 \x01(\x0c\x12\x1f\n\x13latest_block_height\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13\x65\x61rliest_block_hash\x18\x05 \x01(\x0c\x12\x19\n\x11\x65\x61rliest_app_hash\x18\x06 \x01(\x0c\x12!\n\x15\x65\x61rliest_block_height\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15max_peer_block_height\x18\t \x01(\x04\x42\x02\x30\x01\x12%\n\x18\x63ore_chain_locked_height\x18\n \x01(\rH\x00\x88\x01\x01\x42\x1b\n\x19_core_chain_locked_height\x1a\x43\n\x07Network\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\t\x12\x13\n\x0bpeers_count\x18\x02 \x01(\r\x12\x11\n\tlistening\x18\x03 \x01(\x08\x1a\x85\x02\n\tStateSync\x12\x1d\n\x11total_synced_time\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1a\n\x0eremaining_time\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x17\n\x0ftotal_snapshots\x18\x03 \x01(\r\x12\"\n\x16\x63hunk_process_avg_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x0fsnapshot_height\x18\x05 \x01(\x04\x42\x02\x30\x01\x12!\n\x15snapshot_chunks_count\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x11\x62\x61\x63kfilled_blocks\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15\x62\x61\x63kfill_blocks_total\x18\x08 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07version\"\xb1\x01\n\x1cGetCurrentQuorumsInfoRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest.GetCurrentQuorumsInfoRequestV0H\x00\x1a \n\x1eGetCurrentQuorumsInfoRequestV0B\t\n\x07version\"\xa1\x05\n\x1dGetCurrentQuorumsInfoResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.GetCurrentQuorumsInfoResponseV0H\x00\x1a\x46\n\x0bValidatorV0\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07node_ip\x18\x02 \x01(\t\x12\x11\n\tis_banned\x18\x03 \x01(\x08\x1a\xaf\x01\n\x0eValidatorSetV0\x12\x13\n\x0bquorum_hash\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ore_height\x18\x02 \x01(\r\x12U\n\x07members\x18\x03 \x03(\x0b\x32\x44.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorV0\x12\x1c\n\x14threshold_public_key\x18\x04 \x01(\x0c\x1a\x92\x02\n\x1fGetCurrentQuorumsInfoResponseV0\x12\x15\n\rquorum_hashes\x18\x01 \x03(\x0c\x12\x1b\n\x13\x63urrent_quorum_hash\x18\x02 \x01(\x0c\x12_\n\x0evalidator_sets\x18\x03 \x03(\x0b\x32G.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorSetV0\x12\x1b\n\x13last_block_proposer\x18\x04 \x01(\x0c\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf4\x01\n\x1fGetIdentityTokenBalancesRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest.GetIdentityTokenBalancesRequestV0H\x00\x1aZ\n!GetIdentityTokenBalancesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xad\x05\n GetIdentityTokenBalancesResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0H\x00\x1a\x8f\x04\n\"GetIdentityTokenBalancesResponseV0\x12\x86\x01\n\x0etoken_balances\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aG\n\x11TokenBalanceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x9a\x01\n\rTokenBalances\x12\x88\x01\n\x0etoken_balances\x18\x01 \x03(\x0b\x32p.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n!GetIdentitiesTokenBalancesRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest.GetIdentitiesTokenBalancesRequestV0H\x00\x1a\\\n#GetIdentitiesTokenBalancesRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xf2\x05\n\"GetIdentitiesTokenBalancesResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0H\x00\x1a\xce\x04\n$GetIdentitiesTokenBalancesResponseV0\x12\x9b\x01\n\x17identity_token_balances\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aR\n\x19IdentityTokenBalanceEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\xb7\x01\n\x15IdentityTokenBalances\x12\x9d\x01\n\x17identity_token_balances\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xe8\x01\n\x1cGetIdentityTokenInfosRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest.GetIdentityTokenInfosRequestV0H\x00\x1aW\n\x1eGetIdentityTokenInfosRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x98\x06\n\x1dGetIdentityTokenInfosResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0H\x00\x1a\x83\x05\n\x1fGetIdentityTokenInfosResponseV0\x12z\n\x0btoken_infos\x18\x01 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb0\x01\n\x0eTokenInfoEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x82\x01\n\x04info\x18\x02 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x8a\x01\n\nTokenInfos\x12|\n\x0btoken_infos\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n\x1eGetIdentitiesTokenInfosRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest.GetIdentitiesTokenInfosRequestV0H\x00\x1aY\n GetIdentitiesTokenInfosRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xca\x06\n\x1fGetIdentitiesTokenInfosResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0H\x00\x1a\xaf\x05\n!GetIdentitiesTokenInfosResponseV0\x12\x8f\x01\n\x14identity_token_infos\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.IdentityTokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb7\x01\n\x0eTokenInfoEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x86\x01\n\x04info\x18\x02 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x97\x01\n\x12IdentityTokenInfos\x12\x80\x01\n\x0btoken_infos\x18\x01 \x03(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbf\x01\n\x17GetTokenStatusesRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetTokenStatusesRequest.GetTokenStatusesRequestV0H\x00\x1a=\n\x19GetTokenStatusesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xe7\x04\n\x18GetTokenStatusesResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0H\x00\x1a\xe1\x03\n\x1aGetTokenStatusesResponseV0\x12v\n\x0etoken_statuses\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x44\n\x10TokenStatusEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x13\n\x06paused\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\t\n\x07_paused\x1a\x88\x01\n\rTokenStatuses\x12w\n\x0etoken_statuses\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusEntryB\x08\n\x06resultB\t\n\x07version\"\xef\x01\n#GetTokenDirectPurchasePricesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest.GetTokenDirectPurchasePricesRequestV0H\x00\x1aI\n%GetTokenDirectPurchasePricesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x8b\t\n$GetTokenDirectPurchasePricesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0H\x00\x1a\xe1\x07\n&GetTokenDirectPurchasePricesResponseV0\x12\xa9\x01\n\x1ctoken_direct_purchase_prices\x18\x01 \x01(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePricesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xa7\x01\n\x0fPricingSchedule\x12\x93\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PriceForQuantity\x1a\xe4\x01\n\x1dTokenDirectPurchasePriceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x15\n\x0b\x66ixed_price\x18\x02 \x01(\x04H\x00\x12\x90\x01\n\x0evariable_price\x18\x03 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PricingScheduleH\x00\x42\x07\n\x05price\x1a\xc8\x01\n\x19TokenDirectPurchasePrices\x12\xaa\x01\n\x1btoken_direct_purchase_price\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePriceEntryB\x08\n\x06resultB\t\n\x07version\"\xce\x01\n\x1bGetTokenContractInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenContractInfoRequest.GetTokenContractInfoRequestV0H\x00\x1a@\n\x1dGetTokenContractInfoRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xfb\x03\n\x1cGetTokenContractInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0H\x00\x1a\xe9\x02\n\x1eGetTokenContractInfoResponseV0\x12|\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0.TokenContractInfoDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aM\n\x15TokenContractInfoData\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xef\x04\n)GetTokenPreProgrammedDistributionsRequest\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0H\x00\x1a\xb6\x03\n+GetTokenPreProgrammedDistributionsRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x98\x01\n\rstart_at_info\x18\x02 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0.StartAtInfoH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x1a\x9a\x01\n\x0bStartAtInfo\x12\x15\n\rstart_time_ms\x18\x01 \x01(\x04\x12\x1c\n\x0fstart_recipient\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12%\n\x18start_recipient_included\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_start_recipientB\x1b\n\x19_start_recipient_includedB\x10\n\x0e_start_at_infoB\x08\n\x06_limitB\t\n\x07version\"\xec\x07\n*GetTokenPreProgrammedDistributionsResponse\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0H\x00\x1a\xaf\x06\n,GetTokenPreProgrammedDistributionsResponseV0\x12\xa5\x01\n\x13token_distributions\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a>\n\x16TokenDistributionEntry\x12\x14\n\x0crecipient_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x1a\xd4\x01\n\x1bTokenTimedDistributionEntry\x12\x11\n\ttimestamp\x18\x01 \x01(\x04\x12\xa1\x01\n\rdistributions\x18\x02 \x03(\x0b\x32\x89\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionEntry\x1a\xc3\x01\n\x12TokenDistributions\x12\xac\x01\n\x13token_distributions\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenTimedDistributionEntryB\x08\n\x06resultB\t\n\x07version\"\x82\x04\n-GetTokenPerpetualDistributionLastClaimRequest\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.GetTokenPerpetualDistributionLastClaimRequestV0H\x00\x1aI\n\x11\x43ontractTokenInfo\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\r\x1a\xf1\x01\n/GetTokenPerpetualDistributionLastClaimRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12v\n\rcontract_info\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.ContractTokenInfoH\x00\x88\x01\x01\x12\x13\n\x0bidentity_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x10\n\x0e_contract_infoB\t\n\x07version\"\x93\x05\n.GetTokenPerpetualDistributionLastClaimResponse\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0H\x00\x1a\xca\x03\n0GetTokenPerpetualDistributionLastClaimResponseV0\x12\x9f\x01\n\nlast_claim\x18\x01 \x01(\x0b\x32\x88\x01.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0.LastClaimInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\rLastClaimInfo\x12\x1a\n\x0ctimestamp_ms\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1a\n\x0c\x62lock_height\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x0f\n\x05\x65poch\x18\x03 \x01(\rH\x00\x12\x13\n\traw_bytes\x18\x04 \x01(\x0cH\x00\x42\t\n\x07paid_atB\x08\n\x06resultB\t\n\x07version\"\xca\x01\n\x1aGetTokenTotalSupplyRequest\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest.GetTokenTotalSupplyRequestV0H\x00\x1a?\n\x1cGetTokenTotalSupplyRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xaf\x04\n\x1bGetTokenTotalSupplyResponse\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0H\x00\x1a\xa0\x03\n\x1dGetTokenTotalSupplyResponseV0\x12\x88\x01\n\x12token_total_supply\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0.TokenTotalSupplyEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\x15TokenTotalSupplyEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x30\n(total_aggregated_amount_in_user_accounts\x18\x02 \x01(\x04\x12\x1b\n\x13total_system_amount\x18\x03 \x01(\x04\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x01\n\x13GetGroupInfoRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetGroupInfoRequest.GetGroupInfoRequestV0H\x00\x1a\\\n\x15GetGroupInfoRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xd4\x05\n\x14GetGroupInfoResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0H\x00\x1a\xda\x04\n\x16GetGroupInfoResponseV0\x12\x66\n\ngroup_info\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x98\x01\n\x0eGroupInfoEntry\x12h\n\x07members\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x02 \x01(\r\x1a\x8a\x01\n\tGroupInfo\x12n\n\ngroup_info\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoEntryH\x00\x88\x01\x01\x42\r\n\x0b_group_infoB\x08\n\x06resultB\t\n\x07version\"\xed\x03\n\x14GetGroupInfosRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfosRequest.GetGroupInfosRequestV0H\x00\x1au\n\x1cStartAtGroupContractPosition\x12%\n\x1dstart_group_contract_position\x18\x01 \x01(\r\x12.\n&start_group_contract_position_included\x18\x02 \x01(\x08\x1a\xfc\x01\n\x16GetGroupInfosRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12{\n start_at_group_contract_position\x18\x02 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupInfosRequest.StartAtGroupContractPositionH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x42#\n!_start_at_group_contract_positionB\x08\n\x06_countB\t\n\x07version\"\xff\x05\n\x15GetGroupInfosResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0H\x00\x1a\x82\x05\n\x17GetGroupInfosResponseV0\x12j\n\x0bgroup_infos\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\xc3\x01\n\x16GroupPositionInfoEntry\x12\x1f\n\x17group_contract_position\x18\x01 \x01(\r\x12j\n\x07members\x18\x02 \x03(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x03 \x01(\r\x1a\x82\x01\n\nGroupInfos\x12t\n\x0bgroup_infos\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupPositionInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbe\x04\n\x16GetGroupActionsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetGroupActionsRequest.GetGroupActionsRequestV0H\x00\x1aL\n\x0fStartAtActionId\x12\x17\n\x0fstart_action_id\x18\x01 \x01(\x0c\x12 \n\x18start_action_id_included\x18\x02 \x01(\x08\x1a\xc8\x02\n\x18GetGroupActionsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12N\n\x06status\x18\x03 \x01(\x0e\x32>.org.dash.platform.dapi.v0.GetGroupActionsRequest.ActionStatus\x12\x62\n\x12start_at_action_id\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetGroupActionsRequest.StartAtActionIdH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\x15\n\x13_start_at_action_idB\x08\n\x06_count\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\xd6\x1e\n\x17GetGroupActionsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0H\x00\x1a\xd3\x1d\n\x19GetGroupActionsResponseV0\x12r\n\rgroup_actions\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a[\n\tMintEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0crecipient_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a[\n\tBurnEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0c\x62urn_from_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aJ\n\x0b\x46reezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aL\n\rUnfreezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x66\n\x17\x44\x65stroyFrozenFundsEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x13SharedEncryptedNote\x12\x18\n\x10sender_key_index\x18\x01 \x01(\r\x12\x1b\n\x13recipient_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a{\n\x15PersonalEncryptedNote\x12!\n\x19root_encryption_key_index\x18\x01 \x01(\r\x12\'\n\x1f\x64\x65rivation_encryption_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a\xe9\x01\n\x14\x45mergencyActionEvent\x12\x81\x01\n\x0b\x61\x63tion_type\x18\x01 \x01(\x0e\x32l.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.ActionType\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\"#\n\nActionType\x12\t\n\x05PAUSE\x10\x00\x12\n\n\x06RESUME\x10\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x16TokenConfigUpdateEvent\x12 \n\x18token_config_update_item\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xe6\x03\n\x1eUpdateDirectPurchasePriceEvent\x12\x15\n\x0b\x66ixed_price\x18\x01 \x01(\x04H\x00\x12\x95\x01\n\x0evariable_price\x18\x02 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingScheduleH\x00\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xac\x01\n\x0fPricingSchedule\x12\x98\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantityB\x07\n\x05priceB\x0e\n\x0c_public_note\x1a\xfc\x02\n\x10GroupActionEvent\x12n\n\x0btoken_event\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEventH\x00\x12t\n\x0e\x64ocument_event\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEventH\x00\x12t\n\x0e\x63ontract_event\x18\x03 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEventH\x00\x42\x0c\n\nevent_type\x1a\x8b\x01\n\rDocumentEvent\x12r\n\x06\x63reate\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEventH\x00\x42\x06\n\x04type\x1a/\n\x13\x44ocumentCreateEvent\x12\x18\n\x10\x63reated_document\x18\x01 \x01(\x0c\x1a/\n\x13\x43ontractUpdateEvent\x12\x18\n\x10updated_contract\x18\x01 \x01(\x0c\x1a\x8b\x01\n\rContractEvent\x12r\n\x06update\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEventH\x00\x42\x06\n\x04type\x1a\xd1\x07\n\nTokenEvent\x12\x66\n\x04mint\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEventH\x00\x12\x66\n\x04\x62urn\x18\x02 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEventH\x00\x12j\n\x06\x66reeze\x18\x03 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.FreezeEventH\x00\x12n\n\x08unfreeze\x18\x04 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UnfreezeEventH\x00\x12\x84\x01\n\x14\x64\x65stroy_frozen_funds\x18\x05 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEventH\x00\x12}\n\x10\x65mergency_action\x18\x06 \x01(\x0b\x32\x61.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEventH\x00\x12\x82\x01\n\x13token_config_update\x18\x07 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEventH\x00\x12\x83\x01\n\x0cupdate_price\x18\x08 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEventH\x00\x42\x06\n\x04type\x1a\x93\x01\n\x10GroupActionEntry\x12\x11\n\taction_id\x18\x01 \x01(\x0c\x12l\n\x05\x65vent\x18\x02 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent\x1a\x84\x01\n\x0cGroupActions\x12t\n\rgroup_actions\x18\x01 \x03(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEntryB\x08\n\x06resultB\t\n\x07version\"\x88\x03\n\x1cGetGroupActionSignersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.GetGroupActionSignersRequestV0H\x00\x1a\xce\x01\n\x1eGetGroupActionSignersRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12T\n\x06status\x18\x03 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.ActionStatus\x12\x11\n\taction_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x8b\x05\n\x1dGetGroupActionSignersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0H\x00\x1a\xf6\x03\n\x1fGetGroupActionSignersResponseV0\x12\x8b\x01\n\x14group_action_signers\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x35\n\x11GroupActionSigner\x12\x11\n\tsigner_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x91\x01\n\x12GroupActionSigners\x12{\n\x07signers\x18\x01 \x03(\x0b\x32j.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignerB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x15GetAddressInfoRequest\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetAddressInfoRequest.GetAddressInfoRequestV0H\x00\x1a\x39\n\x17GetAddressInfoRequestV0\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x85\x01\n\x10\x41\x64\x64ressInfoEntry\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12J\n\x11\x62\x61lance_and_nonce\x18\x02 \x01(\x0b\x32*.org.dash.platform.dapi.v0.BalanceAndNonceH\x00\x88\x01\x01\x42\x14\n\x12_balance_and_nonce\"1\n\x0f\x42\x61lanceAndNonce\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\x04\x12\r\n\x05nonce\x18\x02 \x01(\r\"_\n\x12\x41\x64\x64ressInfoEntries\x12I\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x03(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntry\"m\n\x14\x41\x64\x64ressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_balance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1c\n\x0e\x61\x64\x64_to_balance\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x42\x0b\n\toperation\"x\n\x1a\x42lockAddressBalanceChanges\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12@\n\x07\x63hanges\x18\x02 \x03(\x0b\x32/.org.dash.platform.dapi.v0.AddressBalanceChange\"k\n\x1b\x41\x64\x64ressBalanceUpdateEntries\x12L\n\rblock_changes\x18\x01 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.BlockAddressBalanceChanges\"\xe1\x02\n\x16GetAddressInfoResponse\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetAddressInfoResponse.GetAddressInfoResponseV0H\x00\x1a\xe1\x01\n\x18GetAddressInfoResponseV0\x12I\n\x12\x61\x64\x64ress_info_entry\x18\x01 \x01(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x01\n\x18GetAddressesInfosRequest\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetAddressesInfosRequest.GetAddressesInfosRequestV0H\x00\x1a>\n\x1aGetAddressesInfosRequestV0\x12\x11\n\taddresses\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf1\x02\n\x19GetAddressesInfosResponse\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetAddressesInfosResponse.GetAddressesInfosResponseV0H\x00\x1a\xe8\x01\n\x1bGetAddressesInfosResponseV0\x12M\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x01(\x0b\x32-.org.dash.platform.dapi.v0.AddressInfoEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x1dGetAddressesTrunkStateRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest.GetAddressesTrunkStateRequestV0H\x00\x1a!\n\x1fGetAddressesTrunkStateRequestV0B\t\n\x07version\"\xaa\x02\n\x1eGetAddressesTrunkStateResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse.GetAddressesTrunkStateResponseV0H\x00\x1a\x92\x01\n GetAddressesTrunkStateResponseV0\x12/\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf0\x01\n\x1eGetAddressesBranchStateRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest.GetAddressesBranchStateRequestV0H\x00\x1aY\n GetAddressesBranchStateRequestV0\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\r\x12\x19\n\x11\x63heckpoint_height\x18\x03 \x01(\x04\x42\t\n\x07version\"\xd1\x01\n\x1fGetAddressesBranchStateResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse.GetAddressesBranchStateResponseV0H\x00\x1a\x37\n!GetAddressesBranchStateResponseV0\x12\x12\n\nmerk_proof\x18\x02 \x01(\x0c\x42\t\n\x07version\"\x9e\x02\n%GetRecentAddressBalanceChangesRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest.GetRecentAddressBalanceChangesRequestV0H\x00\x1ar\n\'GetRecentAddressBalanceChangesRequestV0\x12\x18\n\x0cstart_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1e\n\x16start_height_exclusive\x18\x03 \x01(\x08\x42\t\n\x07version\"\xb8\x03\n&GetRecentAddressBalanceChangesResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse.GetRecentAddressBalanceChangesResponseV0H\x00\x1a\x88\x02\n(GetRecentAddressBalanceChangesResponseV0\x12`\n\x1e\x61\x64\x64ress_balance_update_entries\x18\x01 \x01(\x0b\x32\x36.org.dash.platform.dapi.v0.AddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"G\n\x16\x42lockHeightCreditEntry\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x07\x63redits\x18\x02 \x01(\x04\x42\x02\x30\x01\"\xb0\x01\n\x1d\x43ompactedAddressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_credits\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12V\n\x19\x61\x64\x64_to_credits_operations\x18\x03 \x01(\x0b\x32\x31.org.dash.platform.dapi.v0.AddToCreditsOperationsH\x00\x42\x0b\n\toperation\"\\\n\x16\x41\x64\x64ToCreditsOperations\x12\x42\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x31.org.dash.platform.dapi.v0.BlockHeightCreditEntry\"\xae\x01\n#CompactedBlockAddressBalanceChanges\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1c\n\x10\x65nd_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12I\n\x07\x63hanges\x18\x03 \x03(\x0b\x32\x38.org.dash.platform.dapi.v0.CompactedAddressBalanceChange\"\x87\x01\n$CompactedAddressBalanceUpdateEntries\x12_\n\x17\x63ompacted_block_changes\x18\x01 \x03(\x0b\x32>.org.dash.platform.dapi.v0.CompactedBlockAddressBalanceChanges\"\xa9\x02\n.GetRecentCompactedAddressBalanceChangesRequest\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest.GetRecentCompactedAddressBalanceChangesRequestV0H\x00\x1a\x61\n0GetRecentCompactedAddressBalanceChangesRequestV0\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf0\x03\n/GetRecentCompactedAddressBalanceChangesResponse\x12\x8a\x01\n\x02v0\x18\x01 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse.GetRecentCompactedAddressBalanceChangesResponseV0H\x00\x1a\xa4\x02\n1GetRecentCompactedAddressBalanceChangesResponseV0\x12s\n(compacted_address_balance_update_entries\x18\x01 \x01(\x0b\x32?.org.dash.platform.dapi.v0.CompactedAddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf4\x01\n GetShieldedEncryptedNotesRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0H\x00\x1aW\n\"GetShieldedEncryptedNotesRequestV0\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xbc\x05\n!GetShieldedEncryptedNotesResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0H\x00\x1a\x9b\x04\n#GetShieldedEncryptedNotesResponseV0\x12\x8a\x01\n\x0f\x65ncrypted_notes\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aW\n\rEncryptedNote\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x16\n\x0e\x65ncrypted_note\x18\x03 \x01(\x0c\x12\x0e\n\x06\x63v_net\x18\x04 \x01(\x0c\x1a\x91\x01\n\x0e\x45ncryptedNotes\x12\x7f\n\x07\x65ntries\x18\x01 \x03(\x0b\x32n.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNoteB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x19GetShieldedAnchorsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0H\x00\x1a,\n\x1bGetShieldedAnchorsRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb1\x03\n\x1aGetShieldedAnchorsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0H\x00\x1a\xa5\x02\n\x1cGetShieldedAnchorsResponseV0\x12m\n\x07\x61nchors\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0.AnchorsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x07\x41nchors\x12\x0f\n\x07\x61nchors\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd8\x01\n\"GetMostRecentShieldedAnchorRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0H\x00\x1a\x35\n$GetMostRecentShieldedAnchorRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xdc\x02\n#GetMostRecentShieldedAnchorResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse.GetMostRecentShieldedAnchorResponseV0H\x00\x1a\xb5\x01\n%GetMostRecentShieldedAnchorResponseV0\x12\x10\n\x06\x61nchor\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x01\n\x1bGetShieldedPoolStateRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0H\x00\x1a.\n\x1dGetShieldedPoolStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xcb\x02\n\x1cGetShieldedPoolStateResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse.GetShieldedPoolStateResponseV0H\x00\x1a\xb9\x01\n\x1eGetShieldedPoolStateResponseV0\x12\x1b\n\rtotal_balance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc0\x01\n\x1cGetShieldedNotesCountRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0H\x00\x1a/\n\x1eGetShieldedNotesCountRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd3\x02\n\x1dGetShieldedNotesCountResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse.GetShieldedNotesCountResponseV0H\x00\x1a\xbe\x01\n\x1fGetShieldedNotesCountResponseV0\x12\x1f\n\x11total_notes_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd4\x01\n\x1cGetShieldedNullifiersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0H\x00\x1a\x43\n\x1eGetShieldedNullifiersRequestV0\x12\x12\n\nnullifiers\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x86\x05\n\x1dGetShieldedNullifiersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0H\x00\x1a\xf1\x03\n\x1fGetShieldedNullifiersResponseV0\x12\x88\x01\n\x12nullifier_statuses\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x0fNullifierStatus\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x10\n\x08is_spent\x18\x02 \x01(\x08\x1a\x8e\x01\n\x11NullifierStatuses\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusB\x08\n\x06resultB\t\n\x07version*Z\n\nKeyPurpose\x12\x12\n\x0e\x41UTHENTICATION\x10\x00\x12\x0e\n\nENCRYPTION\x10\x01\x12\x0e\n\nDECRYPTION\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x12\n\n\x06VOTING\x10\x05\x32\xce\x44\n\x08Platform\x12\x93\x01\n\x18\x62roadcastStateTransition\x12:.org.dash.platform.dapi.v0.BroadcastStateTransitionRequest\x1a;.org.dash.platform.dapi.v0.BroadcastStateTransitionResponse\x12l\n\x0bgetIdentity\x12-.org.dash.platform.dapi.v0.GetIdentityRequest\x1a..org.dash.platform.dapi.v0.GetIdentityResponse\x12x\n\x0fgetIdentityKeys\x12\x31.org.dash.platform.dapi.v0.GetIdentityKeysRequest\x1a\x32.org.dash.platform.dapi.v0.GetIdentityKeysResponse\x12\x96\x01\n\x19getIdentitiesContractKeys\x12;.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest\x1a<.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse\x12{\n\x10getIdentityNonce\x12\x32.org.dash.platform.dapi.v0.GetIdentityNonceRequest\x1a\x33.org.dash.platform.dapi.v0.GetIdentityNonceResponse\x12\x93\x01\n\x18getIdentityContractNonce\x12:.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse\x12\x81\x01\n\x12getIdentityBalance\x12\x34.org.dash.platform.dapi.v0.GetIdentityBalanceRequest\x1a\x35.org.dash.platform.dapi.v0.GetIdentityBalanceResponse\x12\x8a\x01\n\x15getIdentitiesBalances\x12\x37.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse\x12\xa2\x01\n\x1dgetIdentityBalanceAndRevision\x12?.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest\x1a@.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse\x12\xaf\x01\n#getEvonodesProposedEpochBlocksByIds\x12\x45.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12\xb3\x01\n%getEvonodesProposedEpochBlocksByRange\x12G.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12x\n\x0fgetDataContract\x12\x31.org.dash.platform.dapi.v0.GetDataContractRequest\x1a\x32.org.dash.platform.dapi.v0.GetDataContractResponse\x12\x8d\x01\n\x16getDataContractHistory\x12\x38.org.dash.platform.dapi.v0.GetDataContractHistoryRequest\x1a\x39.org.dash.platform.dapi.v0.GetDataContractHistoryResponse\x12{\n\x10getDataContracts\x12\x32.org.dash.platform.dapi.v0.GetDataContractsRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x81\x01\n\x12getDocumentHistory\x12\x34.org.dash.platform.dapi.v0.GetDocumentHistoryRequest\x1a\x35.org.dash.platform.dapi.v0.GetDocumentHistoryResponse\x12o\n\x0cgetDocuments\x12..org.dash.platform.dapi.v0.GetDocumentsRequest\x1a/.org.dash.platform.dapi.v0.GetDocumentsResponse\x12\x99\x01\n\x1agetIdentityByPublicKeyHash\x12<.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest\x1a=.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse\x12\xb4\x01\n#getIdentityByNonUniquePublicKeyHash\x12\x45.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest\x1a\x46.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse\x12\x9f\x01\n\x1cwaitForStateTransitionResult\x12>.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest\x1a?.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse\x12\x81\x01\n\x12getConsensusParams\x12\x34.org.dash.platform.dapi.v0.GetConsensusParamsRequest\x1a\x35.org.dash.platform.dapi.v0.GetConsensusParamsResponse\x12\xa5\x01\n\x1egetProtocolVersionUpgradeState\x12@.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest\x1a\x41.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse\x12\xb4\x01\n#getProtocolVersionUpgradeVoteStatus\x12\x45.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest\x1a\x46.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse\x12r\n\rgetEpochsInfo\x12/.org.dash.platform.dapi.v0.GetEpochsInfoRequest\x1a\x30.org.dash.platform.dapi.v0.GetEpochsInfoResponse\x12\x8d\x01\n\x16getFinalizedEpochInfos\x12\x38.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest\x1a\x39.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse\x12\x8a\x01\n\x15getContestedResources\x12\x37.org.dash.platform.dapi.v0.GetContestedResourcesRequest\x1a\x38.org.dash.platform.dapi.v0.GetContestedResourcesResponse\x12\xa2\x01\n\x1dgetContestedResourceVoteState\x12?.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest\x1a@.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse\x12\xba\x01\n%getContestedResourceVotersForIdentity\x12G.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest\x1aH.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse\x12\xae\x01\n!getContestedResourceIdentityVotes\x12\x43.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest\x1a\x44.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse\x12\x8a\x01\n\x15getVotePollsByEndDate\x12\x37.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest\x1a\x38.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse\x12\xa5\x01\n\x1egetPrefundedSpecializedBalance\x12@.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest\x1a\x41.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse\x12\x96\x01\n\x19getTotalCreditsInPlatform\x12;.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest\x1a<.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse\x12x\n\x0fgetPathElements\x12\x31.org.dash.platform.dapi.v0.GetPathElementsRequest\x1a\x32.org.dash.platform.dapi.v0.GetPathElementsResponse\x12\x66\n\tgetStatus\x12+.org.dash.platform.dapi.v0.GetStatusRequest\x1a,.org.dash.platform.dapi.v0.GetStatusResponse\x12\x8a\x01\n\x15getCurrentQuorumsInfo\x12\x37.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest\x1a\x38.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse\x12\x93\x01\n\x18getIdentityTokenBalances\x12:.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse\x12\x99\x01\n\x1agetIdentitiesTokenBalances\x12<.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest\x1a=.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse\x12\x8a\x01\n\x15getIdentityTokenInfos\x12\x37.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse\x12\x90\x01\n\x17getIdentitiesTokenInfos\x12\x39.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest\x1a:.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse\x12{\n\x10getTokenStatuses\x12\x32.org.dash.platform.dapi.v0.GetTokenStatusesRequest\x1a\x33.org.dash.platform.dapi.v0.GetTokenStatusesResponse\x12\x9f\x01\n\x1cgetTokenDirectPurchasePrices\x12>.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest\x1a?.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse\x12\x87\x01\n\x14getTokenContractInfo\x12\x36.org.dash.platform.dapi.v0.GetTokenContractInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetTokenContractInfoResponse\x12\xb1\x01\n\"getTokenPreProgrammedDistributions\x12\x44.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest\x1a\x45.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse\x12\xbd\x01\n&getTokenPerpetualDistributionLastClaim\x12H.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest\x1aI.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse\x12\x84\x01\n\x13getTokenTotalSupply\x12\x35.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest\x1a\x36.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse\x12o\n\x0cgetGroupInfo\x12..org.dash.platform.dapi.v0.GetGroupInfoRequest\x1a/.org.dash.platform.dapi.v0.GetGroupInfoResponse\x12r\n\rgetGroupInfos\x12/.org.dash.platform.dapi.v0.GetGroupInfosRequest\x1a\x30.org.dash.platform.dapi.v0.GetGroupInfosResponse\x12x\n\x0fgetGroupActions\x12\x31.org.dash.platform.dapi.v0.GetGroupActionsRequest\x1a\x32.org.dash.platform.dapi.v0.GetGroupActionsResponse\x12\x8a\x01\n\x15getGroupActionSigners\x12\x37.org.dash.platform.dapi.v0.GetGroupActionSignersRequest\x1a\x38.org.dash.platform.dapi.v0.GetGroupActionSignersResponse\x12u\n\x0egetAddressInfo\x12\x30.org.dash.platform.dapi.v0.GetAddressInfoRequest\x1a\x31.org.dash.platform.dapi.v0.GetAddressInfoResponse\x12~\n\x11getAddressesInfos\x12\x33.org.dash.platform.dapi.v0.GetAddressesInfosRequest\x1a\x34.org.dash.platform.dapi.v0.GetAddressesInfosResponse\x12\x8d\x01\n\x16getAddressesTrunkState\x12\x38.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest\x1a\x39.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse\x12\x90\x01\n\x17getAddressesBranchState\x12\x39.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest\x1a:.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse\x12\xa5\x01\n\x1egetRecentAddressBalanceChanges\x12@.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest\x1a\x41.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse\x12\xc0\x01\n\'getRecentCompactedAddressBalanceChanges\x12I.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest\x1aJ.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse\x12\x96\x01\n\x19getShieldedEncryptedNotes\x12;.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest\x1a<.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse\x12\x81\x01\n\x12getShieldedAnchors\x12\x34.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest\x1a\x35.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse\x12\x9c\x01\n\x1bgetMostRecentShieldedAnchor\x12=.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest\x1a>.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse\x12\x87\x01\n\x14getShieldedPoolState\x12\x36.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest\x1a\x37.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse\x12\x8a\x01\n\x15getShieldedNotesCount\x12\x37.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse\x12\x8a\x01\n\x15getShieldedNullifiers\x12\x37.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNullifiersResponseb\x06proto3' + serialized_pb=b'\n\x0eplatform.proto\x12\x19org.dash.platform.dapi.v0\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x81\x01\n\x05Proof\x12\x15\n\rgrovedb_proof\x18\x01 \x01(\x0c\x12\x13\n\x0bquorum_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\r\n\x05round\x18\x04 \x01(\r\x12\x15\n\rblock_id_hash\x18\x05 \x01(\x0c\x12\x13\n\x0bquorum_type\x18\x06 \x01(\r\"\x98\x01\n\x10ResponseMetadata\x12\x12\n\x06height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12 \n\x18\x63ore_chain_locked_height\x18\x02 \x01(\r\x12\r\n\x05\x65poch\x18\x03 \x01(\r\x12\x13\n\x07time_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\t\"L\n\x1dStateTransitionBroadcastError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\";\n\x1f\x42roadcastStateTransitionRequest\x12\x18\n\x10state_transition\x18\x01 \x01(\x0c\"\"\n BroadcastStateTransitionResponse\"\xa4\x01\n\x12GetIdentityRequest\x12P\n\x02v0\x18\x01 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetIdentityRequest.GetIdentityRequestV0H\x00\x1a\x31\n\x14GetIdentityRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xc1\x01\n\x17GetIdentityNonceRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityNonceRequest.GetIdentityNonceRequestV0H\x00\x1a?\n\x19GetIdentityNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf6\x01\n\x1fGetIdentityContractNonceRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest.GetIdentityContractNonceRequestV0H\x00\x1a\\\n!GetIdentityContractNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xc0\x01\n\x19GetIdentityBalanceRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetIdentityBalanceRequest.GetIdentityBalanceRequestV0H\x00\x1a\x38\n\x1bGetIdentityBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xec\x01\n$GetIdentityBalanceAndRevisionRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest.GetIdentityBalanceAndRevisionRequestV0H\x00\x1a\x43\n&GetIdentityBalanceAndRevisionRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9e\x02\n\x13GetIdentityResponse\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetIdentityResponse.GetIdentityResponseV0H\x00\x1a\xa7\x01\n\x15GetIdentityResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x02\n\x18GetIdentityNonceResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetIdentityNonceResponse.GetIdentityNonceResponseV0H\x00\x1a\xb6\x01\n\x1aGetIdentityNonceResponseV0\x12\x1c\n\x0eidentity_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe5\x02\n GetIdentityContractNonceResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse.GetIdentityContractNonceResponseV0H\x00\x1a\xc7\x01\n\"GetIdentityContractNonceResponseV0\x12%\n\x17identity_contract_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n\x1aGetIdentityBalanceResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetIdentityBalanceResponse.GetIdentityBalanceResponseV0H\x00\x1a\xb1\x01\n\x1cGetIdentityBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb1\x04\n%GetIdentityBalanceAndRevisionResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0H\x00\x1a\x84\x03\n\'GetIdentityBalanceAndRevisionResponseV0\x12\x9b\x01\n\x14\x62\x61lance_and_revision\x18\x01 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0.BalanceAndRevisionH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x12\x42\x61lanceAndRevision\x12\x13\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x14\n\x08revision\x18\x02 \x01(\x04\x42\x02\x30\x01\x42\x08\n\x06resultB\t\n\x07version\"\xd1\x01\n\x0eKeyRequestType\x12\x36\n\x08\x61ll_keys\x18\x01 \x01(\x0b\x32\".org.dash.platform.dapi.v0.AllKeysH\x00\x12@\n\rspecific_keys\x18\x02 \x01(\x0b\x32\'.org.dash.platform.dapi.v0.SpecificKeysH\x00\x12:\n\nsearch_key\x18\x03 \x01(\x0b\x32$.org.dash.platform.dapi.v0.SearchKeyH\x00\x42\t\n\x07request\"\t\n\x07\x41llKeys\"\x1f\n\x0cSpecificKeys\x12\x0f\n\x07key_ids\x18\x01 \x03(\r\"\xb6\x01\n\tSearchKey\x12I\n\x0bpurpose_map\x18\x01 \x03(\x0b\x32\x34.org.dash.platform.dapi.v0.SearchKey.PurposeMapEntry\x1a^\n\x0fPurposeMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.org.dash.platform.dapi.v0.SecurityLevelMap:\x02\x38\x01\"\xbf\x02\n\x10SecurityLevelMap\x12]\n\x12security_level_map\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.SecurityLevelMap.SecurityLevelMapEntry\x1aw\n\x15SecurityLevelMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12M\n\x05value\x18\x02 \x01(\x0e\x32>.org.dash.platform.dapi.v0.SecurityLevelMap.KeyKindRequestType:\x02\x38\x01\"S\n\x12KeyKindRequestType\x12\x1f\n\x1b\x43URRENT_KEY_OF_KIND_REQUEST\x10\x00\x12\x1c\n\x18\x41LL_KEYS_OF_KIND_REQUEST\x10\x01\"\xda\x02\n\x16GetIdentityKeysRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetIdentityKeysRequest.GetIdentityKeysRequestV0H\x00\x1a\xda\x01\n\x18GetIdentityKeysRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12?\n\x0crequest_type\x18\x02 \x01(\x0b\x32).org.dash.platform.dapi.v0.KeyRequestType\x12+\n\x05limit\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\x99\x03\n\x17GetIdentityKeysResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0H\x00\x1a\x96\x02\n\x19GetIdentityKeysResponseV0\x12\x61\n\x04keys\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0.KeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x04Keys\x12\x12\n\nkeys_bytes\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xef\x02\n GetIdentitiesContractKeysRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest.GetIdentitiesContractKeysRequestV0H\x00\x1a\xd1\x01\n\"GetIdentitiesContractKeysRequestV0\x12\x16\n\x0eidentities_ids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\x1f\n\x12\x64ocument_type_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x37\n\x08purposes\x18\x04 \x03(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x15\n\x13_document_type_nameB\t\n\x07version\"\xdf\x06\n!GetIdentitiesContractKeysResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0H\x00\x1a\xbe\x05\n#GetIdentitiesContractKeysResponseV0\x12\x8a\x01\n\x0fidentities_keys\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentitiesKeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aY\n\x0bPurposeKeys\x12\x36\n\x07purpose\x18\x01 \x01(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\x12\n\nkeys_bytes\x18\x02 \x03(\x0c\x1a\x9f\x01\n\x0cIdentityKeys\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12z\n\x04keys\x18\x02 \x03(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.PurposeKeys\x1a\x90\x01\n\x0eIdentitiesKeys\x12~\n\x07\x65ntries\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentityKeysB\x08\n\x06resultB\t\n\x07version\"\xa4\x02\n*GetEvonodesProposedEpochBlocksByIdsRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest.GetEvonodesProposedEpochBlocksByIdsRequestV0H\x00\x1ah\n,GetEvonodesProposedEpochBlocksByIdsRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0b\n\x03ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x08\n\x06_epochB\t\n\x07version\"\x92\x06\n&GetEvonodesProposedEpochBlocksResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0H\x00\x1a\xe2\x04\n(GetEvonodesProposedEpochBlocksResponseV0\x12\xb1\x01\n#evonodes_proposed_block_counts_info\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodesProposedBlocksH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x15\x45vonodeProposedBlocks\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01\x1a\xc4\x01\n\x16\x45vonodesProposedBlocks\x12\xa9\x01\n\x1e\x65vonodes_proposed_block_counts\x18\x01 \x03(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodeProposedBlocksB\x08\n\x06resultB\t\n\x07version\"\xf2\x02\n,GetEvonodesProposedEpochBlocksByRangeRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest.GetEvonodesProposedEpochBlocksByRangeRequestV0H\x00\x1a\xaf\x01\n.GetEvonodesProposedEpochBlocksByRangeRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x02\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x04 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_epochB\x08\n\x06_limitB\t\n\x07version\"\xcd\x01\n\x1cGetIdentitiesBalancesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest.GetIdentitiesBalancesRequestV0H\x00\x1a<\n\x1eGetIdentitiesBalancesRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x05\n\x1dGetIdentitiesBalancesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0H\x00\x1a\x8a\x04\n\x1fGetIdentitiesBalancesResponseV0\x12\x8a\x01\n\x13identities_balances\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentitiesBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aL\n\x0fIdentityBalance\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x18\n\x07\x62\x61lance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x8f\x01\n\x12IdentitiesBalances\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentityBalanceB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x16GetDataContractRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetDataContractRequest.GetDataContractRequestV0H\x00\x1a\x35\n\x18GetDataContractRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xb3\x02\n\x17GetDataContractResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractResponse.GetDataContractResponseV0H\x00\x1a\xb0\x01\n\x19GetDataContractResponseV0\x12\x17\n\rdata_contract\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb9\x01\n\x17GetDataContractsRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractsRequest.GetDataContractsRequestV0H\x00\x1a\x37\n\x19GetDataContractsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xcf\x04\n\x18GetDataContractsResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetDataContractsResponse.GetDataContractsResponseV0H\x00\x1a[\n\x11\x44\x61taContractEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x32\n\rdata_contract\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x1au\n\rDataContracts\x12\x64\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32\x45.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractEntry\x1a\xf5\x01\n\x1aGetDataContractsResponseV0\x12[\n\x0e\x64\x61ta_contracts\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc5\x02\n\x1dGetDataContractHistoryRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetDataContractHistoryRequest.GetDataContractHistoryRequestV0H\x00\x1a\xb0\x01\n\x1fGetDataContractHistoryRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xb2\x05\n\x1eGetDataContractHistoryResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0H\x00\x1a\x9a\x04\n GetDataContractHistoryResponseV0\x12\x8f\x01\n\x15\x64\x61ta_contract_history\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a;\n\x18\x44\x61taContractHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\xaa\x01\n\x13\x44\x61taContractHistory\x12\x92\x01\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32s.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xde\x15\n\x13GetDocumentsRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV0H\x00\x12R\n\x02v1\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1H\x00\x1a\xfe\x02\n\x12\x44ocumentFieldValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x19\n\x0bint64_value\x18\x02 \x01(\x12\x42\x02\x30\x01H\x00\x12\x1a\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12\x0e\n\x04text\x18\x05 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x12[\n\x04list\x18\x07 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.ValueListH\x00\x12\x14\n\nnull_value\x18\x08 \x01(\x08H\x00\x1a^\n\tValueList\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueB\t\n\x07variant\x1a\xbe\x01\n\x0bWhereClause\x12\r\n\x05\x66ield\x18\x01 \x01(\t\x12N\n\x08operator\x18\x02 \x01(\x0e\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator\x12P\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x1a\xa4\x01\n\x0fHavingAggregate\x12Y\n\x08\x66unction\x18\x01 \x01(\x0e\x32G.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"\'\n\x08\x46unction\x12\t\n\x05\x43OUNT\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x07\n\x03\x41VG\x10\x02\x1a\xf9\x03\n\x0cHavingClause\x12Q\n\taggregate\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate\x12V\n\x08operator\x18\x02 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator\x12R\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueH\x00\"\xe0\x01\n\x08Operator\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x05\x12\x0b\n\x07\x42\x45TWEEN\x10\x06\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x07\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x08\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\t\x12\x06\n\x02IN\x10\nB\x07\n\x05right\x1a\x90\x01\n\x0bOrderClause\x12\x0f\n\x05\x66ield\x18\x01 \x01(\tH\x00\x12S\n\taggregate\x18\x03 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregateH\x00\x12\x11\n\tascending\x18\x02 \x01(\x08\x42\x08\n\x06target\x1a\xbb\x01\n\x15GetDocumentsRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12\r\n\x05where\x18\x03 \x01(\x0c\x12\x10\n\x08order_by\x18\x04 \x01(\x0c\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x42\x07\n\x05start\x1a\xf3\x05\n\x15GetDocumentsRequestV1\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x12\\\n\x07selects\x18\t \x03(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select\x12\x10\n\x08group_by\x18\n \x03(\t\x12K\n\x06having\x18\x0b \x03(\x0b\x32;.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause\x12\x13\n\x06offset\x18\x0c \x01(\rH\x02\x88\x01\x01\x1a\xc9\x01\n\x06Select\x12\x66\n\x08\x66unction\x18\x01 \x01(\x0e\x32T.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"H\n\x08\x46unction\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x12\x07\n\x03SUM\x10\x02\x12\x07\n\x03\x41VG\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x42\x07\n\x05startB\x08\n\x06_limitB\t\n\x07_offset\"\xe7\x01\n\rWhereOperator\x12\t\n\x05\x45QUAL\x10\x00\x12\x10\n\x0cGREATER_THAN\x10\x01\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x02\x12\r\n\tLESS_THAN\x10\x03\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x04\x12\x0b\n\x07\x42\x45TWEEN\x10\x05\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x06\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x07\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\x08\x12\x06\n\x02IN\x10\t\x12\x0f\n\x0bSTARTS_WITH\x10\nB\t\n\x07version\"\xe7\x15\n\x14GetDocumentsResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0H\x00\x12T\n\x02v1\x18\x02 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1H\x00\x1a\x9b\x02\n\x16GetDocumentsResponseV0\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0.DocumentsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x42\x08\n\x06result\x1a\xf9\x11\n\x16GetDocumentsResponseV1\x12\x61\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x1aL\n\nCountEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07_in_key\x1ar\n\x0c\x43ountEntries\x12\x62\n\x07\x65ntries\x18\x01 \x03(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntry\x1a\xa0\x01\n\x0c\x43ountResults\x12\x1d\n\x0f\x61ggregate_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x66\n\x07\x65ntries\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x42\t\n\x07variant\x1aH\n\x08SumEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x0f\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1an\n\nSumEntries\x12`\n\x07\x65ntries\x18\x01 \x03(\x0b\x32O.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntry\x1a\x9a\x01\n\nSumResults\x12\x1b\n\raggregate_sum\x18\x01 \x01(\x12\x42\x02\x30\x01H\x00\x12\x64\n\x07\x65ntries\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntriesH\x00\x42\t\n\x07variant\x1a_\n\x0c\x41verageEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x04 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1av\n\x0e\x41verageEntries\x12\x64\n\x07\x65ntries\x18\x01 \x03(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntry\x1a\x36\n\x10\x41verageAggregate\x12\x11\n\x05\x63ount\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x02 \x01(\x12\x42\x02\x30\x01\x1a\xfb\x01\n\x0e\x41verageResults\x12t\n\x11\x61ggregate_average\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregateH\x00\x12h\n\x07\x65ntries\x18\x02 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntriesH\x00\x42\t\n\x07variant\x1aZ\n\x0bRankedEntry\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x13\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x11\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01H\x00\x12\r\n\x03\x61vg\x18\x04 \x01(\x01H\x00\x42\x07\n\x05value\x1a\x9a\x01\n\rRankedEntries\x12\x63\n\x07\x65ntries\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry\x12\x18\n\x07skipped\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_skipped\x1a\x9b\x04\n\nResultData\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResultsH\x00\x12\x61\n\x04sums\x18\x03 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResultsH\x00\x12i\n\x08\x61verages\x18\x04 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResultsH\x00\x12\x66\n\x06ranked\x18\x05 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntriesH\x00\x42\t\n\x07variantB\x08\n\x06resultB\t\n\x07version\"\xf4\x02\n\x19GetDocumentHistoryRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentHistoryRequest.GetDocumentHistoryRequestV0H\x00\x1a\xeb\x01\n\x1bGetDocumentHistoryRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x03 \x01(\x0c\x12+\n\x05limit\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x07 \x01(\x08\x42\t\n\x07version\"\xf7\x04\n\x1aGetDocumentHistoryResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0H\x00\x1a\xeb\x03\n\x1cGetDocumentHistoryResponseV0\x12~\n\x10\x64ocument_history\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x37\n\x14\x44ocumentHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\x95\x01\n\x0f\x44ocumentHistory\x12\x81\x01\n\x10\x64ocument_entries\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xed\x01\n!GetIdentityByPublicKeyHashRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest.GetIdentityByPublicKeyHashRequestV0H\x00\x1aM\n#GetIdentityByPublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xda\x02\n\"GetIdentityByPublicKeyHashResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse.GetIdentityByPublicKeyHashResponseV0H\x00\x1a\xb6\x01\n$GetIdentityByPublicKeyHashResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n*GetIdentityByNonUniquePublicKeyHashRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest.GetIdentityByNonUniquePublicKeyHashRequestV0H\x00\x1a\x80\x01\n,GetIdentityByNonUniquePublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\x18\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x0e\n\x0c_start_afterB\t\n\x07version\"\xd6\x06\n+GetIdentityByNonUniquePublicKeyHashResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0H\x00\x1a\x96\x05\n-GetIdentityByNonUniquePublicKeyHashResponseV0\x12\x9a\x01\n\x08identity\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityResponseH\x00\x12\x9d\x01\n\x05proof\x18\x02 \x01(\x0b\x32\x8b\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityProvedResponseH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x10IdentityResponse\x12\x15\n\x08identity\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_identity\x1a\xa6\x01\n\x16IdentityProvedResponse\x12P\n&grovedb_identity_public_key_hash_proof\x18\x01 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12!\n\x14identity_proof_bytes\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x17\n\x15_identity_proof_bytesB\x08\n\x06resultB\t\n\x07version\"\xfb\x01\n#WaitForStateTransitionResultRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest.WaitForStateTransitionResultRequestV0H\x00\x1aU\n%WaitForStateTransitionResultRequestV0\x12\x1d\n\x15state_transition_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x99\x03\n$WaitForStateTransitionResultResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.WaitForStateTransitionResultResponseV0H\x00\x1a\xef\x01\n&WaitForStateTransitionResultResponseV0\x12I\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x38.org.dash.platform.dapi.v0.StateTransitionBroadcastErrorH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x19GetConsensusParamsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetConsensusParamsRequest.GetConsensusParamsRequestV0H\x00\x1a<\n\x1bGetConsensusParamsRequestV0\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9c\x04\n\x1aGetConsensusParamsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetConsensusParamsResponse.GetConsensusParamsResponseV0H\x00\x1aP\n\x14\x43onsensusParamsBlock\x12\x11\n\tmax_bytes\x18\x01 \x01(\t\x12\x0f\n\x07max_gas\x18\x02 \x01(\t\x12\x14\n\x0ctime_iota_ms\x18\x03 \x01(\t\x1a\x62\n\x17\x43onsensusParamsEvidence\x12\x1a\n\x12max_age_num_blocks\x18\x01 \x01(\t\x12\x18\n\x10max_age_duration\x18\x02 \x01(\t\x12\x11\n\tmax_bytes\x18\x03 \x01(\t\x1a\xda\x01\n\x1cGetConsensusParamsResponseV0\x12Y\n\x05\x62lock\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsBlock\x12_\n\x08\x65vidence\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsEvidenceB\t\n\x07version\"\xe4\x01\n%GetProtocolVersionUpgradeStateRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest.GetProtocolVersionUpgradeStateRequestV0H\x00\x1a\x38\n\'GetProtocolVersionUpgradeStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb5\x05\n&GetProtocolVersionUpgradeStateResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0H\x00\x1a\x85\x04\n(GetProtocolVersionUpgradeStateResponseV0\x12\x87\x01\n\x08versions\x18\x01 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x96\x01\n\x08Versions\x12\x89\x01\n\x08versions\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionEntry\x1a:\n\x0cVersionEntry\x12\x16\n\x0eversion_number\x18\x01 \x01(\r\x12\x12\n\nvote_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xa3\x02\n*GetProtocolVersionUpgradeVoteStatusRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest.GetProtocolVersionUpgradeVoteStatusRequestV0H\x00\x1ag\n,GetProtocolVersionUpgradeVoteStatusRequestV0\x12\x19\n\x11start_pro_tx_hash\x18\x01 \x01(\x0c\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xef\x05\n+GetProtocolVersionUpgradeVoteStatusResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0H\x00\x1a\xaf\x04\n-GetProtocolVersionUpgradeVoteStatusResponseV0\x12\x98\x01\n\x08versions\x18\x01 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xaf\x01\n\x0eVersionSignals\x12\x9c\x01\n\x0fversion_signals\x18\x01 \x03(\x0b\x32\x82\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignal\x1a\x35\n\rVersionSignal\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07version\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xf5\x01\n\x14GetEpochsInfoRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetEpochsInfoRequest.GetEpochsInfoRequestV0H\x00\x1a|\n\x16GetEpochsInfoRequestV0\x12\x31\n\x0bstart_epoch\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x11\n\tascending\x18\x03 \x01(\x08\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\x99\x05\n\x15GetEpochsInfoResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0H\x00\x1a\x9c\x04\n\x17GetEpochsInfoResponseV0\x12\x65\n\x06\x65pochs\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1au\n\nEpochInfos\x12g\n\x0b\x65poch_infos\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfo\x1a\xa6\x01\n\tEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x16\n\nstart_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xbf\x02\n\x1dGetFinalizedEpochInfosRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest.GetFinalizedEpochInfosRequestV0H\x00\x1a\xaa\x01\n\x1fGetFinalizedEpochInfosRequestV0\x12\x19\n\x11start_epoch_index\x18\x01 \x01(\r\x12\"\n\x1astart_epoch_index_included\x18\x02 \x01(\x08\x12\x17\n\x0f\x65nd_epoch_index\x18\x03 \x01(\r\x12 \n\x18\x65nd_epoch_index_included\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xbd\t\n\x1eGetFinalizedEpochInfosResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0H\x00\x1a\xa5\x08\n GetFinalizedEpochInfosResponseV0\x12\x80\x01\n\x06\x65pochs\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xa4\x01\n\x13\x46inalizedEpochInfos\x12\x8c\x01\n\x15\x66inalized_epoch_infos\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfo\x1a\x9f\x04\n\x12\x46inalizedEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x1c\n\x10\x66irst_block_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\x12!\n\x15total_blocks_in_epoch\x18\x07 \x01(\x04\x42\x02\x30\x01\x12*\n\"next_epoch_start_core_block_height\x18\x08 \x01(\r\x12!\n\x15total_processing_fees\x18\t \x01(\x04\x42\x02\x30\x01\x12*\n\x1etotal_distributed_storage_fees\x18\n \x01(\x04\x42\x02\x30\x01\x12&\n\x1atotal_created_storage_fees\x18\x0b \x01(\x04\x42\x02\x30\x01\x12\x1e\n\x12\x63ore_block_rewards\x18\x0c \x01(\x04\x42\x02\x30\x01\x12\x81\x01\n\x0f\x62lock_proposers\x18\r \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.BlockProposer\x1a\x39\n\rBlockProposer\x12\x13\n\x0bproposer_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x62lock_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xde\x04\n\x1cGetContestedResourcesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0H\x00\x1a\xcc\x03\n\x1eGetContestedResourcesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x1a\n\x12start_index_values\x18\x04 \x03(\x0c\x12\x18\n\x10\x65nd_index_values\x18\x05 \x03(\x0c\x12\x89\x01\n\x13start_at_value_info\x18\x06 \x01(\x0b\x32g.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0.StartAtValueInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1a\x45\n\x10StartAtValueInfo\x12\x13\n\x0bstart_value\x18\x01 \x01(\x0c\x12\x1c\n\x14start_value_included\x18\x02 \x01(\x08\x42\x16\n\x14_start_at_value_infoB\x08\n\x06_countB\t\n\x07version\"\x88\x04\n\x1dGetContestedResourcesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0H\x00\x1a\xf3\x02\n\x1fGetContestedResourcesResponseV0\x12\x95\x01\n\x19\x63ontested_resource_values\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0.ContestedResourceValuesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a<\n\x17\x43ontestedResourceValues\x12!\n\x19\x63ontested_resource_values\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x05\n\x1cGetVotePollsByEndDateRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0H\x00\x1a\xc0\x04\n\x1eGetVotePollsByEndDateRequestV0\x12\x84\x01\n\x0fstart_time_info\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.StartAtTimeInfoH\x00\x88\x01\x01\x12\x80\x01\n\rend_time_info\x18\x02 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.EndAtTimeInfoH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x13\n\x06offset\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x11\n\tascending\x18\x05 \x01(\x08\x12\r\n\x05prove\x18\x06 \x01(\x08\x1aI\n\x0fStartAtTimeInfo\x12\x19\n\rstart_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13start_time_included\x18\x02 \x01(\x08\x1a\x43\n\rEndAtTimeInfo\x12\x17\n\x0b\x65nd_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x65nd_time_included\x18\x02 \x01(\x08\x42\x12\n\x10_start_time_infoB\x10\n\x0e_end_time_infoB\x08\n\x06_limitB\t\n\x07_offsetB\t\n\x07version\"\x83\x06\n\x1dGetVotePollsByEndDateResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0H\x00\x1a\xee\x04\n\x1fGetVotePollsByEndDateResponseV0\x12\x9c\x01\n\x18vote_polls_by_timestamps\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestampsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aV\n\x1eSerializedVotePollsByTimestamp\x12\x15\n\ttimestamp\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x15serialized_vote_polls\x18\x02 \x03(\x0c\x1a\xd7\x01\n\x1fSerializedVotePollsByTimestamps\x12\x99\x01\n\x18vote_polls_by_timestamps\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestamp\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xff\x06\n$GetContestedResourceVoteStateRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0H\x00\x1a\xd5\x05\n&GetContestedResourceVoteStateRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x86\x01\n\x0bresult_type\x18\x05 \x01(\x0e\x32q.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.ResultType\x12\x36\n.allow_include_locked_and_abstaining_vote_tally\x18\x06 \x01(\x08\x12\xa3\x01\n\x18start_at_identifier_info\x18\x07 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x08 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\"I\n\nResultType\x12\r\n\tDOCUMENTS\x10\x00\x12\x0e\n\nVOTE_TALLY\x10\x01\x12\x1c\n\x18\x44OCUMENTS_AND_VOTE_TALLY\x10\x02\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\x94\x0c\n%GetContestedResourceVoteStateResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0H\x00\x1a\xe7\n\n\'GetContestedResourceVoteStateResponseV0\x12\xae\x01\n\x1d\x63ontested_resource_contenders\x18\x01 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.ContestedResourceContendersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xda\x03\n\x10\x46inishedVoteInfo\x12\xad\x01\n\x15\x66inished_vote_outcome\x18\x01 \x01(\x0e\x32\x8d\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfo.FinishedVoteOutcome\x12\x1f\n\x12won_by_identity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12$\n\x18\x66inished_at_block_height\x18\x03 \x01(\x04\x42\x02\x30\x01\x12%\n\x1d\x66inished_at_core_block_height\x18\x04 \x01(\r\x12%\n\x19\x66inished_at_block_time_ms\x18\x05 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x66inished_at_epoch\x18\x06 \x01(\r\"O\n\x13\x46inishedVoteOutcome\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x16\n\x12NO_PREVIOUS_WINNER\x10\x02\x42\x15\n\x13_won_by_identity_id\x1a\xc4\x03\n\x1b\x43ontestedResourceContenders\x12\x86\x01\n\ncontenders\x18\x01 \x03(\x0b\x32r.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.Contender\x12\x1f\n\x12\x61\x62stain_vote_tally\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0flock_vote_tally\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x9a\x01\n\x12\x66inished_vote_info\x18\x04 \x01(\x0b\x32y.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfoH\x02\x88\x01\x01\x42\x15\n\x13_abstain_vote_tallyB\x12\n\x10_lock_vote_tallyB\x15\n\x13_finished_vote_info\x1ak\n\tContender\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x17\n\nvote_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08\x64ocument\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\r\n\x0b_vote_countB\x0b\n\t_documentB\x08\n\x06resultB\t\n\x07version\"\xd5\x05\n,GetContestedResourceVotersForIdentityRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0H\x00\x1a\x92\x04\n.GetContestedResourceVotersForIdentityRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x15\n\rcontestant_id\x18\x05 \x01(\x0c\x12\xb4\x01\n\x18start_at_identifier_info\x18\x06 \x01(\x0b\x32\x8c\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\xf1\x04\n-GetContestedResourceVotersForIdentityResponse\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0H\x00\x1a\xab\x03\n/GetContestedResourceVotersForIdentityResponseV0\x12\xb6\x01\n\x19\x63ontested_resource_voters\x18\x01 \x01(\x0b\x32\x90\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0.ContestedResourceVotersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x43\n\x17\x43ontestedResourceVoters\x12\x0e\n\x06voters\x18\x01 \x03(\x0c\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xad\x05\n(GetContestedResourceIdentityVotesRequest\x12|\n\x02v0\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0H\x00\x1a\xf7\x03\n*GetContestedResourceIdentityVotesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0forder_ascending\x18\x04 \x01(\x08\x12\xae\x01\n\x1astart_at_vote_poll_id_info\x18\x05 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0.StartAtVotePollIdInfoH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x1a\x61\n\x15StartAtVotePollIdInfo\x12 \n\x18start_at_poll_identifier\x18\x01 \x01(\x0c\x12&\n\x1estart_poll_identifier_included\x18\x02 \x01(\x08\x42\x1d\n\x1b_start_at_vote_poll_id_infoB\t\n\x07version\"\xc8\n\n)GetContestedResourceIdentityVotesResponse\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0H\x00\x1a\x8f\t\n+GetContestedResourceIdentityVotesResponseV0\x12\xa1\x01\n\x05votes\x18\x01 \x01(\x0b\x32\x8f\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xf7\x01\n\x1e\x43ontestedResourceIdentityVotes\x12\xba\x01\n!contested_resource_identity_votes\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVote\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x1a\xad\x02\n\x12ResourceVoteChoice\x12\xad\x01\n\x10vote_choice_type\x18\x01 \x01(\x0e\x32\x92\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoice.VoteChoiceType\x12\x18\n\x0bidentity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\"=\n\x0eVoteChoiceType\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\x0b\n\x07\x41\x42STAIN\x10\x01\x12\x08\n\x04LOCK\x10\x02\x42\x0e\n\x0c_identity_id\x1a\x95\x02\n\x1d\x43ontestedResourceIdentityVote\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\'\n\x1fserialized_index_storage_values\x18\x03 \x03(\x0c\x12\x99\x01\n\x0bvote_choice\x18\x04 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoiceB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n%GetPrefundedSpecializedBalanceRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest.GetPrefundedSpecializedBalanceRequestV0H\x00\x1a\x44\n\'GetPrefundedSpecializedBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xed\x02\n&GetPrefundedSpecializedBalanceResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse.GetPrefundedSpecializedBalanceResponseV0H\x00\x1a\xbd\x01\n(GetPrefundedSpecializedBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd0\x01\n GetTotalCreditsInPlatformRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest.GetTotalCreditsInPlatformRequestV0H\x00\x1a\x33\n\"GetTotalCreditsInPlatformRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd9\x02\n!GetTotalCreditsInPlatformResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse.GetTotalCreditsInPlatformResponseV0H\x00\x1a\xb8\x01\n#GetTotalCreditsInPlatformResponseV0\x12\x15\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x16GetPathElementsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetPathElementsRequest.GetPathElementsRequestV0H\x00\x1a\x45\n\x18GetPathElementsRequestV0\x12\x0c\n\x04path\x18\x01 \x03(\x0c\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xa3\x03\n\x17GetPathElementsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0H\x00\x1a\xa0\x02\n\x19GetPathElementsResponseV0\x12i\n\x08\x65lements\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0.ElementsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1c\n\x08\x45lements\x12\x10\n\x08\x65lements\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\x81\x01\n\x10GetStatusRequest\x12L\n\x02v0\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetStatusRequest.GetStatusRequestV0H\x00\x1a\x14\n\x12GetStatusRequestV0B\t\n\x07version\"\xe4\x10\n\x11GetStatusResponse\x12N\n\x02v0\x18\x01 \x01(\x0b\x32@.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0H\x00\x1a\xf3\x0f\n\x13GetStatusResponseV0\x12Y\n\x07version\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version\x12S\n\x04node\x18\x02 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Node\x12U\n\x05\x63hain\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Chain\x12Y\n\x07network\x18\x04 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Network\x12^\n\nstate_sync\x18\x05 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.StateSync\x12S\n\x04time\x18\x06 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Time\x1a\x82\x05\n\x07Version\x12\x63\n\x08software\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Software\x12\x63\n\x08protocol\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol\x1a^\n\x08Software\x12\x0c\n\x04\x64\x61pi\x18\x01 \x01(\t\x12\x12\n\x05\x64rive\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ntenderdash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x08\n\x06_driveB\r\n\x0b_tenderdash\x1a\xcc\x02\n\x08Protocol\x12p\n\ntenderdash\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Tenderdash\x12\x66\n\x05\x64rive\x18\x02 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Drive\x1a(\n\nTenderdash\x12\x0b\n\x03p2p\x18\x01 \x01(\r\x12\r\n\x05\x62lock\x18\x02 \x01(\r\x1a<\n\x05\x44rive\x12\x0e\n\x06latest\x18\x03 \x01(\r\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\r\x12\x12\n\nnext_epoch\x18\x05 \x01(\r\x1a\x7f\n\x04Time\x12\x11\n\x05local\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x05\x62lock\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12\x18\n\x07genesis\x18\x03 \x01(\x04\x42\x02\x30\x01H\x01\x88\x01\x01\x12\x12\n\x05\x65poch\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_blockB\n\n\x08_genesisB\x08\n\x06_epoch\x1a<\n\x04Node\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x18\n\x0bpro_tx_hash\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_pro_tx_hash\x1a\xb3\x02\n\x05\x43hain\x12\x13\n\x0b\x63\x61tching_up\x18\x01 \x01(\x08\x12\x19\n\x11latest_block_hash\x18\x02 \x01(\x0c\x12\x17\n\x0flatest_app_hash\x18\x03 \x01(\x0c\x12\x1f\n\x13latest_block_height\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13\x65\x61rliest_block_hash\x18\x05 \x01(\x0c\x12\x19\n\x11\x65\x61rliest_app_hash\x18\x06 \x01(\x0c\x12!\n\x15\x65\x61rliest_block_height\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15max_peer_block_height\x18\t \x01(\x04\x42\x02\x30\x01\x12%\n\x18\x63ore_chain_locked_height\x18\n \x01(\rH\x00\x88\x01\x01\x42\x1b\n\x19_core_chain_locked_height\x1a\x43\n\x07Network\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\t\x12\x13\n\x0bpeers_count\x18\x02 \x01(\r\x12\x11\n\tlistening\x18\x03 \x01(\x08\x1a\x85\x02\n\tStateSync\x12\x1d\n\x11total_synced_time\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1a\n\x0eremaining_time\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x17\n\x0ftotal_snapshots\x18\x03 \x01(\r\x12\"\n\x16\x63hunk_process_avg_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x0fsnapshot_height\x18\x05 \x01(\x04\x42\x02\x30\x01\x12!\n\x15snapshot_chunks_count\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x11\x62\x61\x63kfilled_blocks\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15\x62\x61\x63kfill_blocks_total\x18\x08 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07version\"\xb1\x01\n\x1cGetCurrentQuorumsInfoRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest.GetCurrentQuorumsInfoRequestV0H\x00\x1a \n\x1eGetCurrentQuorumsInfoRequestV0B\t\n\x07version\"\xa1\x05\n\x1dGetCurrentQuorumsInfoResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.GetCurrentQuorumsInfoResponseV0H\x00\x1a\x46\n\x0bValidatorV0\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07node_ip\x18\x02 \x01(\t\x12\x11\n\tis_banned\x18\x03 \x01(\x08\x1a\xaf\x01\n\x0eValidatorSetV0\x12\x13\n\x0bquorum_hash\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ore_height\x18\x02 \x01(\r\x12U\n\x07members\x18\x03 \x03(\x0b\x32\x44.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorV0\x12\x1c\n\x14threshold_public_key\x18\x04 \x01(\x0c\x1a\x92\x02\n\x1fGetCurrentQuorumsInfoResponseV0\x12\x15\n\rquorum_hashes\x18\x01 \x03(\x0c\x12\x1b\n\x13\x63urrent_quorum_hash\x18\x02 \x01(\x0c\x12_\n\x0evalidator_sets\x18\x03 \x03(\x0b\x32G.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorSetV0\x12\x1b\n\x13last_block_proposer\x18\x04 \x01(\x0c\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf4\x01\n\x1fGetIdentityTokenBalancesRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest.GetIdentityTokenBalancesRequestV0H\x00\x1aZ\n!GetIdentityTokenBalancesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xad\x05\n GetIdentityTokenBalancesResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0H\x00\x1a\x8f\x04\n\"GetIdentityTokenBalancesResponseV0\x12\x86\x01\n\x0etoken_balances\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aG\n\x11TokenBalanceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x9a\x01\n\rTokenBalances\x12\x88\x01\n\x0etoken_balances\x18\x01 \x03(\x0b\x32p.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n!GetIdentitiesTokenBalancesRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest.GetIdentitiesTokenBalancesRequestV0H\x00\x1a\\\n#GetIdentitiesTokenBalancesRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xf2\x05\n\"GetIdentitiesTokenBalancesResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0H\x00\x1a\xce\x04\n$GetIdentitiesTokenBalancesResponseV0\x12\x9b\x01\n\x17identity_token_balances\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aR\n\x19IdentityTokenBalanceEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\xb7\x01\n\x15IdentityTokenBalances\x12\x9d\x01\n\x17identity_token_balances\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xe8\x01\n\x1cGetIdentityTokenInfosRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest.GetIdentityTokenInfosRequestV0H\x00\x1aW\n\x1eGetIdentityTokenInfosRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x98\x06\n\x1dGetIdentityTokenInfosResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0H\x00\x1a\x83\x05\n\x1fGetIdentityTokenInfosResponseV0\x12z\n\x0btoken_infos\x18\x01 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb0\x01\n\x0eTokenInfoEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x82\x01\n\x04info\x18\x02 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x8a\x01\n\nTokenInfos\x12|\n\x0btoken_infos\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n\x1eGetIdentitiesTokenInfosRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest.GetIdentitiesTokenInfosRequestV0H\x00\x1aY\n GetIdentitiesTokenInfosRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xca\x06\n\x1fGetIdentitiesTokenInfosResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0H\x00\x1a\xaf\x05\n!GetIdentitiesTokenInfosResponseV0\x12\x8f\x01\n\x14identity_token_infos\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.IdentityTokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb7\x01\n\x0eTokenInfoEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x86\x01\n\x04info\x18\x02 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x97\x01\n\x12IdentityTokenInfos\x12\x80\x01\n\x0btoken_infos\x18\x01 \x03(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbf\x01\n\x17GetTokenStatusesRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetTokenStatusesRequest.GetTokenStatusesRequestV0H\x00\x1a=\n\x19GetTokenStatusesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xe7\x04\n\x18GetTokenStatusesResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0H\x00\x1a\xe1\x03\n\x1aGetTokenStatusesResponseV0\x12v\n\x0etoken_statuses\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x44\n\x10TokenStatusEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x13\n\x06paused\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\t\n\x07_paused\x1a\x88\x01\n\rTokenStatuses\x12w\n\x0etoken_statuses\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusEntryB\x08\n\x06resultB\t\n\x07version\"\xef\x01\n#GetTokenDirectPurchasePricesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest.GetTokenDirectPurchasePricesRequestV0H\x00\x1aI\n%GetTokenDirectPurchasePricesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x8b\t\n$GetTokenDirectPurchasePricesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0H\x00\x1a\xe1\x07\n&GetTokenDirectPurchasePricesResponseV0\x12\xa9\x01\n\x1ctoken_direct_purchase_prices\x18\x01 \x01(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePricesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xa7\x01\n\x0fPricingSchedule\x12\x93\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PriceForQuantity\x1a\xe4\x01\n\x1dTokenDirectPurchasePriceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x15\n\x0b\x66ixed_price\x18\x02 \x01(\x04H\x00\x12\x90\x01\n\x0evariable_price\x18\x03 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PricingScheduleH\x00\x42\x07\n\x05price\x1a\xc8\x01\n\x19TokenDirectPurchasePrices\x12\xaa\x01\n\x1btoken_direct_purchase_price\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePriceEntryB\x08\n\x06resultB\t\n\x07version\"\xce\x01\n\x1bGetTokenContractInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenContractInfoRequest.GetTokenContractInfoRequestV0H\x00\x1a@\n\x1dGetTokenContractInfoRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xfb\x03\n\x1cGetTokenContractInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0H\x00\x1a\xe9\x02\n\x1eGetTokenContractInfoResponseV0\x12|\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0.TokenContractInfoDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aM\n\x15TokenContractInfoData\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xef\x04\n)GetTokenPreProgrammedDistributionsRequest\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0H\x00\x1a\xb6\x03\n+GetTokenPreProgrammedDistributionsRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x98\x01\n\rstart_at_info\x18\x02 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0.StartAtInfoH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x1a\x9a\x01\n\x0bStartAtInfo\x12\x15\n\rstart_time_ms\x18\x01 \x01(\x04\x12\x1c\n\x0fstart_recipient\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12%\n\x18start_recipient_included\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_start_recipientB\x1b\n\x19_start_recipient_includedB\x10\n\x0e_start_at_infoB\x08\n\x06_limitB\t\n\x07version\"\xec\x07\n*GetTokenPreProgrammedDistributionsResponse\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0H\x00\x1a\xaf\x06\n,GetTokenPreProgrammedDistributionsResponseV0\x12\xa5\x01\n\x13token_distributions\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a>\n\x16TokenDistributionEntry\x12\x14\n\x0crecipient_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x1a\xd4\x01\n\x1bTokenTimedDistributionEntry\x12\x11\n\ttimestamp\x18\x01 \x01(\x04\x12\xa1\x01\n\rdistributions\x18\x02 \x03(\x0b\x32\x89\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionEntry\x1a\xc3\x01\n\x12TokenDistributions\x12\xac\x01\n\x13token_distributions\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenTimedDistributionEntryB\x08\n\x06resultB\t\n\x07version\"\x82\x04\n-GetTokenPerpetualDistributionLastClaimRequest\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.GetTokenPerpetualDistributionLastClaimRequestV0H\x00\x1aI\n\x11\x43ontractTokenInfo\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\r\x1a\xf1\x01\n/GetTokenPerpetualDistributionLastClaimRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12v\n\rcontract_info\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.ContractTokenInfoH\x00\x88\x01\x01\x12\x13\n\x0bidentity_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x10\n\x0e_contract_infoB\t\n\x07version\"\x93\x05\n.GetTokenPerpetualDistributionLastClaimResponse\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0H\x00\x1a\xca\x03\n0GetTokenPerpetualDistributionLastClaimResponseV0\x12\x9f\x01\n\nlast_claim\x18\x01 \x01(\x0b\x32\x88\x01.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0.LastClaimInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\rLastClaimInfo\x12\x1a\n\x0ctimestamp_ms\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1a\n\x0c\x62lock_height\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x0f\n\x05\x65poch\x18\x03 \x01(\rH\x00\x12\x13\n\traw_bytes\x18\x04 \x01(\x0cH\x00\x42\t\n\x07paid_atB\x08\n\x06resultB\t\n\x07version\"\xca\x01\n\x1aGetTokenTotalSupplyRequest\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest.GetTokenTotalSupplyRequestV0H\x00\x1a?\n\x1cGetTokenTotalSupplyRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xaf\x04\n\x1bGetTokenTotalSupplyResponse\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0H\x00\x1a\xa0\x03\n\x1dGetTokenTotalSupplyResponseV0\x12\x88\x01\n\x12token_total_supply\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0.TokenTotalSupplyEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\x15TokenTotalSupplyEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x30\n(total_aggregated_amount_in_user_accounts\x18\x02 \x01(\x04\x12\x1b\n\x13total_system_amount\x18\x03 \x01(\x04\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x01\n\x13GetGroupInfoRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetGroupInfoRequest.GetGroupInfoRequestV0H\x00\x1a\\\n\x15GetGroupInfoRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xd4\x05\n\x14GetGroupInfoResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0H\x00\x1a\xda\x04\n\x16GetGroupInfoResponseV0\x12\x66\n\ngroup_info\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x98\x01\n\x0eGroupInfoEntry\x12h\n\x07members\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x02 \x01(\r\x1a\x8a\x01\n\tGroupInfo\x12n\n\ngroup_info\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoEntryH\x00\x88\x01\x01\x42\r\n\x0b_group_infoB\x08\n\x06resultB\t\n\x07version\"\xed\x03\n\x14GetGroupInfosRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfosRequest.GetGroupInfosRequestV0H\x00\x1au\n\x1cStartAtGroupContractPosition\x12%\n\x1dstart_group_contract_position\x18\x01 \x01(\r\x12.\n&start_group_contract_position_included\x18\x02 \x01(\x08\x1a\xfc\x01\n\x16GetGroupInfosRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12{\n start_at_group_contract_position\x18\x02 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupInfosRequest.StartAtGroupContractPositionH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x42#\n!_start_at_group_contract_positionB\x08\n\x06_countB\t\n\x07version\"\xff\x05\n\x15GetGroupInfosResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0H\x00\x1a\x82\x05\n\x17GetGroupInfosResponseV0\x12j\n\x0bgroup_infos\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\xc3\x01\n\x16GroupPositionInfoEntry\x12\x1f\n\x17group_contract_position\x18\x01 \x01(\r\x12j\n\x07members\x18\x02 \x03(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x03 \x01(\r\x1a\x82\x01\n\nGroupInfos\x12t\n\x0bgroup_infos\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupPositionInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbe\x04\n\x16GetGroupActionsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetGroupActionsRequest.GetGroupActionsRequestV0H\x00\x1aL\n\x0fStartAtActionId\x12\x17\n\x0fstart_action_id\x18\x01 \x01(\x0c\x12 \n\x18start_action_id_included\x18\x02 \x01(\x08\x1a\xc8\x02\n\x18GetGroupActionsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12N\n\x06status\x18\x03 \x01(\x0e\x32>.org.dash.platform.dapi.v0.GetGroupActionsRequest.ActionStatus\x12\x62\n\x12start_at_action_id\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetGroupActionsRequest.StartAtActionIdH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\x15\n\x13_start_at_action_idB\x08\n\x06_count\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\xd6\x1e\n\x17GetGroupActionsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0H\x00\x1a\xd3\x1d\n\x19GetGroupActionsResponseV0\x12r\n\rgroup_actions\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a[\n\tMintEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0crecipient_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a[\n\tBurnEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0c\x62urn_from_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aJ\n\x0b\x46reezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aL\n\rUnfreezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x66\n\x17\x44\x65stroyFrozenFundsEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x13SharedEncryptedNote\x12\x18\n\x10sender_key_index\x18\x01 \x01(\r\x12\x1b\n\x13recipient_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a{\n\x15PersonalEncryptedNote\x12!\n\x19root_encryption_key_index\x18\x01 \x01(\r\x12\'\n\x1f\x64\x65rivation_encryption_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a\xe9\x01\n\x14\x45mergencyActionEvent\x12\x81\x01\n\x0b\x61\x63tion_type\x18\x01 \x01(\x0e\x32l.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.ActionType\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\"#\n\nActionType\x12\t\n\x05PAUSE\x10\x00\x12\n\n\x06RESUME\x10\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x16TokenConfigUpdateEvent\x12 \n\x18token_config_update_item\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xe6\x03\n\x1eUpdateDirectPurchasePriceEvent\x12\x15\n\x0b\x66ixed_price\x18\x01 \x01(\x04H\x00\x12\x95\x01\n\x0evariable_price\x18\x02 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingScheduleH\x00\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xac\x01\n\x0fPricingSchedule\x12\x98\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantityB\x07\n\x05priceB\x0e\n\x0c_public_note\x1a\xfc\x02\n\x10GroupActionEvent\x12n\n\x0btoken_event\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEventH\x00\x12t\n\x0e\x64ocument_event\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEventH\x00\x12t\n\x0e\x63ontract_event\x18\x03 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEventH\x00\x42\x0c\n\nevent_type\x1a\x8b\x01\n\rDocumentEvent\x12r\n\x06\x63reate\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEventH\x00\x42\x06\n\x04type\x1a/\n\x13\x44ocumentCreateEvent\x12\x18\n\x10\x63reated_document\x18\x01 \x01(\x0c\x1a/\n\x13\x43ontractUpdateEvent\x12\x18\n\x10updated_contract\x18\x01 \x01(\x0c\x1a\x8b\x01\n\rContractEvent\x12r\n\x06update\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEventH\x00\x42\x06\n\x04type\x1a\xd1\x07\n\nTokenEvent\x12\x66\n\x04mint\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEventH\x00\x12\x66\n\x04\x62urn\x18\x02 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEventH\x00\x12j\n\x06\x66reeze\x18\x03 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.FreezeEventH\x00\x12n\n\x08unfreeze\x18\x04 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UnfreezeEventH\x00\x12\x84\x01\n\x14\x64\x65stroy_frozen_funds\x18\x05 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEventH\x00\x12}\n\x10\x65mergency_action\x18\x06 \x01(\x0b\x32\x61.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEventH\x00\x12\x82\x01\n\x13token_config_update\x18\x07 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEventH\x00\x12\x83\x01\n\x0cupdate_price\x18\x08 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEventH\x00\x42\x06\n\x04type\x1a\x93\x01\n\x10GroupActionEntry\x12\x11\n\taction_id\x18\x01 \x01(\x0c\x12l\n\x05\x65vent\x18\x02 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent\x1a\x84\x01\n\x0cGroupActions\x12t\n\rgroup_actions\x18\x01 \x03(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEntryB\x08\n\x06resultB\t\n\x07version\"\x88\x03\n\x1cGetGroupActionSignersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.GetGroupActionSignersRequestV0H\x00\x1a\xce\x01\n\x1eGetGroupActionSignersRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12T\n\x06status\x18\x03 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.ActionStatus\x12\x11\n\taction_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x8b\x05\n\x1dGetGroupActionSignersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0H\x00\x1a\xf6\x03\n\x1fGetGroupActionSignersResponseV0\x12\x8b\x01\n\x14group_action_signers\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x35\n\x11GroupActionSigner\x12\x11\n\tsigner_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x91\x01\n\x12GroupActionSigners\x12{\n\x07signers\x18\x01 \x03(\x0b\x32j.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignerB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x15GetAddressInfoRequest\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetAddressInfoRequest.GetAddressInfoRequestV0H\x00\x1a\x39\n\x17GetAddressInfoRequestV0\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x85\x01\n\x10\x41\x64\x64ressInfoEntry\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12J\n\x11\x62\x61lance_and_nonce\x18\x02 \x01(\x0b\x32*.org.dash.platform.dapi.v0.BalanceAndNonceH\x00\x88\x01\x01\x42\x14\n\x12_balance_and_nonce\"1\n\x0f\x42\x61lanceAndNonce\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\x04\x12\r\n\x05nonce\x18\x02 \x01(\r\"_\n\x12\x41\x64\x64ressInfoEntries\x12I\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x03(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntry\"m\n\x14\x41\x64\x64ressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_balance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1c\n\x0e\x61\x64\x64_to_balance\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x42\x0b\n\toperation\"x\n\x1a\x42lockAddressBalanceChanges\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12@\n\x07\x63hanges\x18\x02 \x03(\x0b\x32/.org.dash.platform.dapi.v0.AddressBalanceChange\"k\n\x1b\x41\x64\x64ressBalanceUpdateEntries\x12L\n\rblock_changes\x18\x01 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.BlockAddressBalanceChanges\"\xe1\x02\n\x16GetAddressInfoResponse\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetAddressInfoResponse.GetAddressInfoResponseV0H\x00\x1a\xe1\x01\n\x18GetAddressInfoResponseV0\x12I\n\x12\x61\x64\x64ress_info_entry\x18\x01 \x01(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x01\n\x18GetAddressesInfosRequest\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetAddressesInfosRequest.GetAddressesInfosRequestV0H\x00\x1a>\n\x1aGetAddressesInfosRequestV0\x12\x11\n\taddresses\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf1\x02\n\x19GetAddressesInfosResponse\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetAddressesInfosResponse.GetAddressesInfosResponseV0H\x00\x1a\xe8\x01\n\x1bGetAddressesInfosResponseV0\x12M\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x01(\x0b\x32-.org.dash.platform.dapi.v0.AddressInfoEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x1dGetAddressesTrunkStateRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest.GetAddressesTrunkStateRequestV0H\x00\x1a!\n\x1fGetAddressesTrunkStateRequestV0B\t\n\x07version\"\xaa\x02\n\x1eGetAddressesTrunkStateResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse.GetAddressesTrunkStateResponseV0H\x00\x1a\x92\x01\n GetAddressesTrunkStateResponseV0\x12/\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf0\x01\n\x1eGetAddressesBranchStateRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest.GetAddressesBranchStateRequestV0H\x00\x1aY\n GetAddressesBranchStateRequestV0\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\r\x12\x19\n\x11\x63heckpoint_height\x18\x03 \x01(\x04\x42\t\n\x07version\"\xd1\x01\n\x1fGetAddressesBranchStateResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse.GetAddressesBranchStateResponseV0H\x00\x1a\x37\n!GetAddressesBranchStateResponseV0\x12\x12\n\nmerk_proof\x18\x02 \x01(\x0c\x42\t\n\x07version\"\x9e\x02\n%GetRecentAddressBalanceChangesRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest.GetRecentAddressBalanceChangesRequestV0H\x00\x1ar\n\'GetRecentAddressBalanceChangesRequestV0\x12\x18\n\x0cstart_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1e\n\x16start_height_exclusive\x18\x03 \x01(\x08\x42\t\n\x07version\"\xb8\x03\n&GetRecentAddressBalanceChangesResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse.GetRecentAddressBalanceChangesResponseV0H\x00\x1a\x88\x02\n(GetRecentAddressBalanceChangesResponseV0\x12`\n\x1e\x61\x64\x64ress_balance_update_entries\x18\x01 \x01(\x0b\x32\x36.org.dash.platform.dapi.v0.AddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"G\n\x16\x42lockHeightCreditEntry\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x07\x63redits\x18\x02 \x01(\x04\x42\x02\x30\x01\"\xb0\x01\n\x1d\x43ompactedAddressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_credits\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12V\n\x19\x61\x64\x64_to_credits_operations\x18\x03 \x01(\x0b\x32\x31.org.dash.platform.dapi.v0.AddToCreditsOperationsH\x00\x42\x0b\n\toperation\"\\\n\x16\x41\x64\x64ToCreditsOperations\x12\x42\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x31.org.dash.platform.dapi.v0.BlockHeightCreditEntry\"\xae\x01\n#CompactedBlockAddressBalanceChanges\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1c\n\x10\x65nd_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12I\n\x07\x63hanges\x18\x03 \x03(\x0b\x32\x38.org.dash.platform.dapi.v0.CompactedAddressBalanceChange\"\x87\x01\n$CompactedAddressBalanceUpdateEntries\x12_\n\x17\x63ompacted_block_changes\x18\x01 \x03(\x0b\x32>.org.dash.platform.dapi.v0.CompactedBlockAddressBalanceChanges\"\xa9\x02\n.GetRecentCompactedAddressBalanceChangesRequest\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest.GetRecentCompactedAddressBalanceChangesRequestV0H\x00\x1a\x61\n0GetRecentCompactedAddressBalanceChangesRequestV0\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf0\x03\n/GetRecentCompactedAddressBalanceChangesResponse\x12\x8a\x01\n\x02v0\x18\x01 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse.GetRecentCompactedAddressBalanceChangesResponseV0H\x00\x1a\xa4\x02\n1GetRecentCompactedAddressBalanceChangesResponseV0\x12s\n(compacted_address_balance_update_entries\x18\x01 \x01(\x0b\x32?.org.dash.platform.dapi.v0.CompactedAddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf4\x01\n GetShieldedEncryptedNotesRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0H\x00\x1aW\n\"GetShieldedEncryptedNotesRequestV0\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xbc\x05\n!GetShieldedEncryptedNotesResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0H\x00\x1a\x9b\x04\n#GetShieldedEncryptedNotesResponseV0\x12\x8a\x01\n\x0f\x65ncrypted_notes\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aW\n\rEncryptedNote\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x16\n\x0e\x65ncrypted_note\x18\x03 \x01(\x0c\x12\x0e\n\x06\x63v_net\x18\x04 \x01(\x0c\x1a\x91\x01\n\x0e\x45ncryptedNotes\x12\x7f\n\x07\x65ntries\x18\x01 \x03(\x0b\x32n.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNoteB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x19GetShieldedAnchorsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0H\x00\x1a,\n\x1bGetShieldedAnchorsRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb1\x03\n\x1aGetShieldedAnchorsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0H\x00\x1a\xa5\x02\n\x1cGetShieldedAnchorsResponseV0\x12m\n\x07\x61nchors\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0.AnchorsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x07\x41nchors\x12\x0f\n\x07\x61nchors\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd8\x01\n\"GetMostRecentShieldedAnchorRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0H\x00\x1a\x35\n$GetMostRecentShieldedAnchorRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xdc\x02\n#GetMostRecentShieldedAnchorResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse.GetMostRecentShieldedAnchorResponseV0H\x00\x1a\xb5\x01\n%GetMostRecentShieldedAnchorResponseV0\x12\x10\n\x06\x61nchor\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x01\n\x1bGetShieldedPoolStateRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0H\x00\x1a.\n\x1dGetShieldedPoolStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xcb\x02\n\x1cGetShieldedPoolStateResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse.GetShieldedPoolStateResponseV0H\x00\x1a\xb9\x01\n\x1eGetShieldedPoolStateResponseV0\x12\x1b\n\rtotal_balance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc0\x01\n\x1cGetShieldedNotesCountRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0H\x00\x1a/\n\x1eGetShieldedNotesCountRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd3\x02\n\x1dGetShieldedNotesCountResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse.GetShieldedNotesCountResponseV0H\x00\x1a\xbe\x01\n\x1fGetShieldedNotesCountResponseV0\x12\x1f\n\x11total_notes_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd4\x01\n\x1cGetShieldedNullifiersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0H\x00\x1a\x43\n\x1eGetShieldedNullifiersRequestV0\x12\x12\n\nnullifiers\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x86\x05\n\x1dGetShieldedNullifiersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0H\x00\x1a\xf1\x03\n\x1fGetShieldedNullifiersResponseV0\x12\x88\x01\n\x12nullifier_statuses\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x0fNullifierStatus\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x10\n\x08is_spent\x18\x02 \x01(\x08\x1a\x8e\x01\n\x11NullifierStatuses\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusB\x08\n\x06resultB\t\n\x07version*Z\n\nKeyPurpose\x12\x12\n\x0e\x41UTHENTICATION\x10\x00\x12\x0e\n\nENCRYPTION\x10\x01\x12\x0e\n\nDECRYPTION\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x12\n\n\x06VOTING\x10\x05\x32\xce\x44\n\x08Platform\x12\x93\x01\n\x18\x62roadcastStateTransition\x12:.org.dash.platform.dapi.v0.BroadcastStateTransitionRequest\x1a;.org.dash.platform.dapi.v0.BroadcastStateTransitionResponse\x12l\n\x0bgetIdentity\x12-.org.dash.platform.dapi.v0.GetIdentityRequest\x1a..org.dash.platform.dapi.v0.GetIdentityResponse\x12x\n\x0fgetIdentityKeys\x12\x31.org.dash.platform.dapi.v0.GetIdentityKeysRequest\x1a\x32.org.dash.platform.dapi.v0.GetIdentityKeysResponse\x12\x96\x01\n\x19getIdentitiesContractKeys\x12;.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest\x1a<.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse\x12{\n\x10getIdentityNonce\x12\x32.org.dash.platform.dapi.v0.GetIdentityNonceRequest\x1a\x33.org.dash.platform.dapi.v0.GetIdentityNonceResponse\x12\x93\x01\n\x18getIdentityContractNonce\x12:.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse\x12\x81\x01\n\x12getIdentityBalance\x12\x34.org.dash.platform.dapi.v0.GetIdentityBalanceRequest\x1a\x35.org.dash.platform.dapi.v0.GetIdentityBalanceResponse\x12\x8a\x01\n\x15getIdentitiesBalances\x12\x37.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse\x12\xa2\x01\n\x1dgetIdentityBalanceAndRevision\x12?.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest\x1a@.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse\x12\xaf\x01\n#getEvonodesProposedEpochBlocksByIds\x12\x45.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12\xb3\x01\n%getEvonodesProposedEpochBlocksByRange\x12G.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12x\n\x0fgetDataContract\x12\x31.org.dash.platform.dapi.v0.GetDataContractRequest\x1a\x32.org.dash.platform.dapi.v0.GetDataContractResponse\x12\x8d\x01\n\x16getDataContractHistory\x12\x38.org.dash.platform.dapi.v0.GetDataContractHistoryRequest\x1a\x39.org.dash.platform.dapi.v0.GetDataContractHistoryResponse\x12{\n\x10getDataContracts\x12\x32.org.dash.platform.dapi.v0.GetDataContractsRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x81\x01\n\x12getDocumentHistory\x12\x34.org.dash.platform.dapi.v0.GetDocumentHistoryRequest\x1a\x35.org.dash.platform.dapi.v0.GetDocumentHistoryResponse\x12o\n\x0cgetDocuments\x12..org.dash.platform.dapi.v0.GetDocumentsRequest\x1a/.org.dash.platform.dapi.v0.GetDocumentsResponse\x12\x99\x01\n\x1agetIdentityByPublicKeyHash\x12<.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest\x1a=.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse\x12\xb4\x01\n#getIdentityByNonUniquePublicKeyHash\x12\x45.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest\x1a\x46.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse\x12\x9f\x01\n\x1cwaitForStateTransitionResult\x12>.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest\x1a?.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse\x12\x81\x01\n\x12getConsensusParams\x12\x34.org.dash.platform.dapi.v0.GetConsensusParamsRequest\x1a\x35.org.dash.platform.dapi.v0.GetConsensusParamsResponse\x12\xa5\x01\n\x1egetProtocolVersionUpgradeState\x12@.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest\x1a\x41.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse\x12\xb4\x01\n#getProtocolVersionUpgradeVoteStatus\x12\x45.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest\x1a\x46.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse\x12r\n\rgetEpochsInfo\x12/.org.dash.platform.dapi.v0.GetEpochsInfoRequest\x1a\x30.org.dash.platform.dapi.v0.GetEpochsInfoResponse\x12\x8d\x01\n\x16getFinalizedEpochInfos\x12\x38.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest\x1a\x39.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse\x12\x8a\x01\n\x15getContestedResources\x12\x37.org.dash.platform.dapi.v0.GetContestedResourcesRequest\x1a\x38.org.dash.platform.dapi.v0.GetContestedResourcesResponse\x12\xa2\x01\n\x1dgetContestedResourceVoteState\x12?.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest\x1a@.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse\x12\xba\x01\n%getContestedResourceVotersForIdentity\x12G.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest\x1aH.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse\x12\xae\x01\n!getContestedResourceIdentityVotes\x12\x43.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest\x1a\x44.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse\x12\x8a\x01\n\x15getVotePollsByEndDate\x12\x37.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest\x1a\x38.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse\x12\xa5\x01\n\x1egetPrefundedSpecializedBalance\x12@.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest\x1a\x41.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse\x12\x96\x01\n\x19getTotalCreditsInPlatform\x12;.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest\x1a<.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse\x12x\n\x0fgetPathElements\x12\x31.org.dash.platform.dapi.v0.GetPathElementsRequest\x1a\x32.org.dash.platform.dapi.v0.GetPathElementsResponse\x12\x66\n\tgetStatus\x12+.org.dash.platform.dapi.v0.GetStatusRequest\x1a,.org.dash.platform.dapi.v0.GetStatusResponse\x12\x8a\x01\n\x15getCurrentQuorumsInfo\x12\x37.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest\x1a\x38.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse\x12\x93\x01\n\x18getIdentityTokenBalances\x12:.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse\x12\x99\x01\n\x1agetIdentitiesTokenBalances\x12<.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest\x1a=.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse\x12\x8a\x01\n\x15getIdentityTokenInfos\x12\x37.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse\x12\x90\x01\n\x17getIdentitiesTokenInfos\x12\x39.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest\x1a:.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse\x12{\n\x10getTokenStatuses\x12\x32.org.dash.platform.dapi.v0.GetTokenStatusesRequest\x1a\x33.org.dash.platform.dapi.v0.GetTokenStatusesResponse\x12\x9f\x01\n\x1cgetTokenDirectPurchasePrices\x12>.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest\x1a?.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse\x12\x87\x01\n\x14getTokenContractInfo\x12\x36.org.dash.platform.dapi.v0.GetTokenContractInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetTokenContractInfoResponse\x12\xb1\x01\n\"getTokenPreProgrammedDistributions\x12\x44.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest\x1a\x45.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse\x12\xbd\x01\n&getTokenPerpetualDistributionLastClaim\x12H.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest\x1aI.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse\x12\x84\x01\n\x13getTokenTotalSupply\x12\x35.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest\x1a\x36.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse\x12o\n\x0cgetGroupInfo\x12..org.dash.platform.dapi.v0.GetGroupInfoRequest\x1a/.org.dash.platform.dapi.v0.GetGroupInfoResponse\x12r\n\rgetGroupInfos\x12/.org.dash.platform.dapi.v0.GetGroupInfosRequest\x1a\x30.org.dash.platform.dapi.v0.GetGroupInfosResponse\x12x\n\x0fgetGroupActions\x12\x31.org.dash.platform.dapi.v0.GetGroupActionsRequest\x1a\x32.org.dash.platform.dapi.v0.GetGroupActionsResponse\x12\x8a\x01\n\x15getGroupActionSigners\x12\x37.org.dash.platform.dapi.v0.GetGroupActionSignersRequest\x1a\x38.org.dash.platform.dapi.v0.GetGroupActionSignersResponse\x12u\n\x0egetAddressInfo\x12\x30.org.dash.platform.dapi.v0.GetAddressInfoRequest\x1a\x31.org.dash.platform.dapi.v0.GetAddressInfoResponse\x12~\n\x11getAddressesInfos\x12\x33.org.dash.platform.dapi.v0.GetAddressesInfosRequest\x1a\x34.org.dash.platform.dapi.v0.GetAddressesInfosResponse\x12\x8d\x01\n\x16getAddressesTrunkState\x12\x38.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest\x1a\x39.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse\x12\x90\x01\n\x17getAddressesBranchState\x12\x39.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest\x1a:.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse\x12\xa5\x01\n\x1egetRecentAddressBalanceChanges\x12@.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest\x1a\x41.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse\x12\xc0\x01\n\'getRecentCompactedAddressBalanceChanges\x12I.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest\x1aJ.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse\x12\x96\x01\n\x19getShieldedEncryptedNotes\x12;.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest\x1a<.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse\x12\x81\x01\n\x12getShieldedAnchors\x12\x34.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest\x1a\x35.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse\x12\x9c\x01\n\x1bgetMostRecentShieldedAnchor\x12=.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest\x1a>.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse\x12\x87\x01\n\x14getShieldedPoolState\x12\x36.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest\x1a\x37.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse\x12\x8a\x01\n\x15getShieldedNotesCount\x12\x37.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse\x12\x8a\x01\n\x15getShieldedNullifiers\x12\x37.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNullifiersResponseb\x06proto3' , dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,]) @@ -62,8 +62,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=65680, - serialized_end=65770, + serialized_start=65780, + serialized_end=65870, ) _sym_db.RegisterEnumDescriptor(_KEYPURPOSE) @@ -130,41 +130,6 @@ ) _sym_db.RegisterEnumDescriptor(_GETDOCUMENTSREQUEST_HAVINGAGGREGATE_FUNCTION) -_GETDOCUMENTSREQUEST_HAVINGRANKING_KIND = _descriptor.EnumDescriptor( - name='Kind', - full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='MIN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='MAX', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TOP', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='BOTTOM', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=11951, - serialized_end=11996, -) -_sym_db.RegisterEnumDescriptor(_GETDOCUMENTSREQUEST_HAVINGRANKING_KIND) - _GETDOCUMENTSREQUEST_HAVINGCLAUSE_OPERATOR = _descriptor.EnumDescriptor( name='Operator', full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator', @@ -230,8 +195,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=12358, - serialized_end=12582, + serialized_start=12105, + serialized_end=12329, ) _sym_db.RegisterEnumDescriptor(_GETDOCUMENTSREQUEST_HAVINGCLAUSE_OPERATOR) @@ -275,8 +240,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=13584, - serialized_end=13656, + serialized_start=13331, + serialized_end=13403, ) _sym_db.RegisterEnumDescriptor(_GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV1_SELECT_FUNCTION) @@ -345,8 +310,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=13689, - serialized_end=13920, + serialized_start=13436, + serialized_end=13667, ) _sym_db.RegisterEnumDescriptor(_GETDOCUMENTSREQUEST_WHEREOPERATOR) @@ -375,8 +340,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=28390, - serialized_end=28463, + serialized_start=28490, + serialized_end=28563, ) _sym_db.RegisterEnumDescriptor(_GETCONTESTEDRESOURCEVOTESTATEREQUEST_GETCONTESTEDRESOURCEVOTESTATEREQUESTV0_RESULTTYPE) @@ -405,8 +370,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=29385, - serialized_end=29464, + serialized_start=29485, + serialized_end=29564, ) _sym_db.RegisterEnumDescriptor(_GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0_FINISHEDVOTEINFO_FINISHEDVOTEOUTCOME) @@ -435,8 +400,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=33093, - serialized_end=33154, + serialized_start=33193, + serialized_end=33254, ) _sym_db.RegisterEnumDescriptor(_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0_RESOURCEVOTECHOICE_VOTECHOICETYPE) @@ -460,8 +425,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=51718, - serialized_end=51756, + serialized_start=51818, + serialized_end=51856, ) _sym_db.RegisterEnumDescriptor(_GETGROUPACTIONSREQUEST_ACTIONSTATUS) @@ -485,8 +450,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=53003, - serialized_end=53038, + serialized_start=53103, + serialized_end=53138, ) _sym_db.RegisterEnumDescriptor(_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT_ACTIONTYPE) @@ -510,8 +475,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=51718, - serialized_end=51756, + serialized_start=51818, + serialized_end=51856, ) _sym_db.RegisterEnumDescriptor(_GETGROUPACTIONSIGNERSREQUEST_ACTIONSTATUS) @@ -3801,50 +3766,6 @@ serialized_end=11830, ) -_GETDOCUMENTSREQUEST_HAVINGRANKING = _descriptor.Descriptor( - name='HavingRanking', - full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='kind', full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.kind', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='n', full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.n', index=1, - number=2, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'0\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _GETDOCUMENTSREQUEST_HAVINGRANKING_KIND, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='_n', full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking._n', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=11833, - serialized_end=12002, -) - _GETDOCUMENTSREQUEST_HAVINGCLAUSE = _descriptor.Descriptor( name='HavingClause', full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause', @@ -3874,13 +3795,6 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='ranking', full_name='org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.ranking', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -3899,8 +3813,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=12005, - serialized_end=12591, + serialized_start=11833, + serialized_end=12338, ) _GETDOCUMENTSREQUEST_ORDERCLAUSE = _descriptor.Descriptor( @@ -3949,8 +3863,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=12594, - serialized_end=12738, + serialized_start=12341, + serialized_end=12485, ) _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV0 = _descriptor.Descriptor( @@ -4034,8 +3948,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=12741, - serialized_end=12928, + serialized_start=12488, + serialized_end=12675, ) _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV1_SELECT = _descriptor.Descriptor( @@ -4073,8 +3987,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13455, - serialized_end=13656, + serialized_start=13202, + serialized_end=13403, ) _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV1 = _descriptor.Descriptor( @@ -4196,8 +4110,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=12931, - serialized_end=13686, + serialized_start=12678, + serialized_end=13433, ) _GETDOCUMENTSREQUEST = _descriptor.Descriptor( @@ -4225,7 +4139,7 @@ ], extensions=[ ], - nested_types=[_GETDOCUMENTSREQUEST_DOCUMENTFIELDVALUE, _GETDOCUMENTSREQUEST_WHERECLAUSE, _GETDOCUMENTSREQUEST_HAVINGAGGREGATE, _GETDOCUMENTSREQUEST_HAVINGRANKING, _GETDOCUMENTSREQUEST_HAVINGCLAUSE, _GETDOCUMENTSREQUEST_ORDERCLAUSE, _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV0, _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV1, ], + nested_types=[_GETDOCUMENTSREQUEST_DOCUMENTFIELDVALUE, _GETDOCUMENTSREQUEST_WHERECLAUSE, _GETDOCUMENTSREQUEST_HAVINGAGGREGATE, _GETDOCUMENTSREQUEST_HAVINGCLAUSE, _GETDOCUMENTSREQUEST_ORDERCLAUSE, _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV0, _GETDOCUMENTSREQUEST_GETDOCUMENTSREQUESTV1, ], enum_types=[ _GETDOCUMENTSREQUEST_WHEREOPERATOR, ], @@ -4241,7 +4155,7 @@ fields=[]), ], serialized_start=10896, - serialized_end=13931, + serialized_end=13678, ) @@ -4272,8 +4186,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14374, - serialized_end=14404, + serialized_start=14121, + serialized_end=14151, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV0 = _descriptor.Descriptor( @@ -4322,8 +4236,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14131, - serialized_end=14414, + serialized_start=13878, + serialized_end=14161, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_DOCUMENTS = _descriptor.Descriptor( @@ -4353,8 +4267,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14374, - serialized_end=14404, + serialized_start=14121, + serialized_end=14151, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRY = _descriptor.Descriptor( @@ -4403,8 +4317,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14688, - serialized_end=14764, + serialized_start=14435, + serialized_end=14511, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRIES = _descriptor.Descriptor( @@ -4434,8 +4348,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14766, - serialized_end=14880, + serialized_start=14513, + serialized_end=14627, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTRESULTS = _descriptor.Descriptor( @@ -4477,8 +4391,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14883, - serialized_end=15043, + serialized_start=14630, + serialized_end=14790, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRY = _descriptor.Descriptor( @@ -4527,8 +4441,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15045, - serialized_end=15117, + serialized_start=14792, + serialized_end=14864, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRIES = _descriptor.Descriptor( @@ -4558,8 +4472,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15119, - serialized_end=15229, + serialized_start=14866, + serialized_end=14976, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMRESULTS = _descriptor.Descriptor( @@ -4601,8 +4515,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15232, - serialized_end=15386, + serialized_start=14979, + serialized_end=15133, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRY = _descriptor.Descriptor( @@ -4658,8 +4572,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15388, - serialized_end=15483, + serialized_start=15135, + serialized_end=15230, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRIES = _descriptor.Descriptor( @@ -4689,8 +4603,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15485, - serialized_end=15603, + serialized_start=15232, + serialized_end=15350, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEAGGREGATE = _descriptor.Descriptor( @@ -4727,8 +4641,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15605, - serialized_end=15659, + serialized_start=15352, + serialized_end=15406, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS = _descriptor.Descriptor( @@ -4770,8 +4684,108 @@ create_key=_descriptor._internal_create_key, fields=[]), ], + serialized_start=15409, + serialized_end=15660, +) + +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY = _descriptor.Descriptor( + name='RankedEntry', + full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.key', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='count', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.count', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'0\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='sum', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.sum', index=2, + number=3, type=18, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'0\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='avg', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.avg', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='value', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.value', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], serialized_start=15662, - serialized_end=15913, + serialized_end=15752, +) + +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES = _descriptor.Descriptor( + name='RankedEntries', + full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='entries', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.entries', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='skipped', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.skipped', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'0\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='_skipped', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries._skipped', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=15755, + serialized_end=15909, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA = _descriptor.Descriptor( @@ -4810,6 +4824,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ranked', full_name='org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.ranked', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -4827,8 +4848,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15916, - serialized_end=16351, + serialized_start=15912, + serialized_end=16451, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1 = _descriptor.Descriptor( @@ -4863,7 +4884,7 @@ ], extensions=[ ], - nested_types=[_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_DOCUMENTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTRESULTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMRESULTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEAGGREGATE, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA, ], + nested_types=[_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_DOCUMENTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTRESULTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMRESULTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEAGGREGATE, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES, _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA, ], enum_types=[ ], serialized_options=None, @@ -4877,8 +4898,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14417, - serialized_end=16361, + serialized_start=14164, + serialized_end=16461, ) _GETDOCUMENTSRESPONSE = _descriptor.Descriptor( @@ -4920,8 +4941,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=13934, - serialized_end=16372, + serialized_start=13681, + serialized_end=16472, ) @@ -4994,8 +5015,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=16501, - serialized_end=16736, + serialized_start=16601, + serialized_end=16836, ) _GETDOCUMENTHISTORYREQUEST = _descriptor.Descriptor( @@ -5030,8 +5051,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=16375, - serialized_end=16747, + serialized_start=16475, + serialized_end=16847, ) @@ -5069,8 +5090,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17153, - serialized_end=17208, + serialized_start=17253, + serialized_end=17308, ) _GETDOCUMENTHISTORYRESPONSE_GETDOCUMENTHISTORYRESPONSEV0_DOCUMENTHISTORY = _descriptor.Descriptor( @@ -5100,8 +5121,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17211, - serialized_end=17360, + serialized_start=17311, + serialized_end=17460, ) _GETDOCUMENTHISTORYRESPONSE_GETDOCUMENTHISTORYRESPONSEV0 = _descriptor.Descriptor( @@ -5150,8 +5171,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=16879, - serialized_end=17370, + serialized_start=16979, + serialized_end=17470, ) _GETDOCUMENTHISTORYRESPONSE = _descriptor.Descriptor( @@ -5186,8 +5207,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=16750, - serialized_end=17381, + serialized_start=16850, + serialized_end=17481, ) @@ -5225,8 +5246,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17533, - serialized_end=17610, + serialized_start=17633, + serialized_end=17710, ) _GETIDENTITYBYPUBLICKEYHASHREQUEST = _descriptor.Descriptor( @@ -5261,8 +5282,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17384, - serialized_end=17621, + serialized_start=17484, + serialized_end=17721, ) @@ -5312,8 +5333,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17777, - serialized_end=17959, + serialized_start=17877, + serialized_end=18059, ) _GETIDENTITYBYPUBLICKEYHASHRESPONSE = _descriptor.Descriptor( @@ -5348,8 +5369,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17624, - serialized_end=17970, + serialized_start=17724, + serialized_end=18070, ) @@ -5399,8 +5420,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18151, - serialized_end=18279, + serialized_start=18251, + serialized_end=18379, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHREQUEST = _descriptor.Descriptor( @@ -5435,8 +5456,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17973, - serialized_end=18290, + serialized_start=18073, + serialized_end=18390, ) @@ -5472,8 +5493,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18903, - serialized_end=18957, + serialized_start=19003, + serialized_end=19057, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSE_GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSEV0_IDENTITYPROVEDRESPONSE = _descriptor.Descriptor( @@ -5515,8 +5536,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18960, - serialized_end=19126, + serialized_start=19060, + serialized_end=19226, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSE_GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSEV0 = _descriptor.Descriptor( @@ -5565,8 +5586,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18474, - serialized_end=19136, + serialized_start=18574, + serialized_end=19236, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSE = _descriptor.Descriptor( @@ -5601,8 +5622,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18293, - serialized_end=19147, + serialized_start=18393, + serialized_end=19247, ) @@ -5640,8 +5661,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19305, - serialized_end=19390, + serialized_start=19405, + serialized_end=19490, ) _WAITFORSTATETRANSITIONRESULTREQUEST = _descriptor.Descriptor( @@ -5676,8 +5697,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19150, - serialized_end=19401, + serialized_start=19250, + serialized_end=19501, ) @@ -5727,8 +5748,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19563, - serialized_end=19802, + serialized_start=19663, + serialized_end=19902, ) _WAITFORSTATETRANSITIONRESULTRESPONSE = _descriptor.Descriptor( @@ -5763,8 +5784,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19404, - serialized_end=19813, + serialized_start=19504, + serialized_end=19913, ) @@ -5802,8 +5823,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19941, - serialized_end=20001, + serialized_start=20041, + serialized_end=20101, ) _GETCONSENSUSPARAMSREQUEST = _descriptor.Descriptor( @@ -5838,8 +5859,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19816, - serialized_end=20012, + serialized_start=19916, + serialized_end=20112, ) @@ -5884,8 +5905,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20143, - serialized_end=20223, + serialized_start=20243, + serialized_end=20323, ) _GETCONSENSUSPARAMSRESPONSE_CONSENSUSPARAMSEVIDENCE = _descriptor.Descriptor( @@ -5929,8 +5950,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20225, - serialized_end=20323, + serialized_start=20325, + serialized_end=20423, ) _GETCONSENSUSPARAMSRESPONSE_GETCONSENSUSPARAMSRESPONSEV0 = _descriptor.Descriptor( @@ -5967,8 +5988,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20326, - serialized_end=20544, + serialized_start=20426, + serialized_end=20644, ) _GETCONSENSUSPARAMSRESPONSE = _descriptor.Descriptor( @@ -6003,8 +6024,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20015, - serialized_end=20555, + serialized_start=20115, + serialized_end=20655, ) @@ -6035,8 +6056,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20719, - serialized_end=20775, + serialized_start=20819, + serialized_end=20875, ) _GETPROTOCOLVERSIONUPGRADESTATEREQUEST = _descriptor.Descriptor( @@ -6071,8 +6092,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20558, - serialized_end=20786, + serialized_start=20658, + serialized_end=20886, ) @@ -6103,8 +6124,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=21251, - serialized_end=21401, + serialized_start=21351, + serialized_end=21501, ) _GETPROTOCOLVERSIONUPGRADESTATERESPONSE_GETPROTOCOLVERSIONUPGRADESTATERESPONSEV0_VERSIONENTRY = _descriptor.Descriptor( @@ -6141,8 +6162,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=21403, - serialized_end=21461, + serialized_start=21503, + serialized_end=21561, ) _GETPROTOCOLVERSIONUPGRADESTATERESPONSE_GETPROTOCOLVERSIONUPGRADESTATERESPONSEV0 = _descriptor.Descriptor( @@ -6191,8 +6212,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20954, - serialized_end=21471, + serialized_start=21054, + serialized_end=21571, ) _GETPROTOCOLVERSIONUPGRADESTATERESPONSE = _descriptor.Descriptor( @@ -6227,8 +6248,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20789, - serialized_end=21482, + serialized_start=20889, + serialized_end=21582, ) @@ -6273,8 +6294,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=21662, - serialized_end=21765, + serialized_start=21762, + serialized_end=21865, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSREQUEST = _descriptor.Descriptor( @@ -6309,8 +6330,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=21485, - serialized_end=21776, + serialized_start=21585, + serialized_end=21876, ) @@ -6341,8 +6362,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=22279, - serialized_end=22454, + serialized_start=22379, + serialized_end=22554, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSE_GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSEV0_VERSIONSIGNAL = _descriptor.Descriptor( @@ -6379,8 +6400,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=22456, - serialized_end=22509, + serialized_start=22556, + serialized_end=22609, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSE_GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSEV0 = _descriptor.Descriptor( @@ -6429,8 +6450,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=21960, - serialized_end=22519, + serialized_start=22060, + serialized_end=22619, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSE = _descriptor.Descriptor( @@ -6465,8 +6486,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=21779, - serialized_end=22530, + serialized_start=21879, + serialized_end=22630, ) @@ -6518,8 +6539,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=22643, - serialized_end=22767, + serialized_start=22743, + serialized_end=22867, ) _GETEPOCHSINFOREQUEST = _descriptor.Descriptor( @@ -6554,8 +6575,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22533, - serialized_end=22778, + serialized_start=22633, + serialized_end=22878, ) @@ -6586,8 +6607,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=23139, - serialized_end=23256, + serialized_start=23239, + serialized_end=23356, ) _GETEPOCHSINFORESPONSE_GETEPOCHSINFORESPONSEV0_EPOCHINFO = _descriptor.Descriptor( @@ -6652,8 +6673,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=23259, - serialized_end=23425, + serialized_start=23359, + serialized_end=23525, ) _GETEPOCHSINFORESPONSE_GETEPOCHSINFORESPONSEV0 = _descriptor.Descriptor( @@ -6702,8 +6723,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22895, - serialized_end=23435, + serialized_start=22995, + serialized_end=23535, ) _GETEPOCHSINFORESPONSE = _descriptor.Descriptor( @@ -6738,8 +6759,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22781, - serialized_end=23446, + serialized_start=22881, + serialized_end=23546, ) @@ -6798,8 +6819,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=23587, - serialized_end=23757, + serialized_start=23687, + serialized_end=23857, ) _GETFINALIZEDEPOCHINFOSREQUEST = _descriptor.Descriptor( @@ -6834,8 +6855,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=23449, - serialized_end=23768, + serialized_start=23549, + serialized_end=23868, ) @@ -6866,8 +6887,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=24194, - serialized_end=24358, + serialized_start=24294, + serialized_end=24458, ) _GETFINALIZEDEPOCHINFOSRESPONSE_GETFINALIZEDEPOCHINFOSRESPONSEV0_FINALIZEDEPOCHINFO = _descriptor.Descriptor( @@ -6981,8 +7002,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=24361, - serialized_end=24904, + serialized_start=24461, + serialized_end=25004, ) _GETFINALIZEDEPOCHINFOSRESPONSE_GETFINALIZEDEPOCHINFOSRESPONSEV0_BLOCKPROPOSER = _descriptor.Descriptor( @@ -7019,8 +7040,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=24906, - serialized_end=24963, + serialized_start=25006, + serialized_end=25063, ) _GETFINALIZEDEPOCHINFOSRESPONSE_GETFINALIZEDEPOCHINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -7069,8 +7090,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=23912, - serialized_end=24973, + serialized_start=24012, + serialized_end=25073, ) _GETFINALIZEDEPOCHINFOSRESPONSE = _descriptor.Descriptor( @@ -7105,8 +7126,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=23771, - serialized_end=24984, + serialized_start=23871, + serialized_end=25084, ) @@ -7144,8 +7165,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=25479, - serialized_end=25548, + serialized_start=25579, + serialized_end=25648, ) _GETCONTESTEDRESOURCESREQUEST_GETCONTESTEDRESOURCESREQUESTV0 = _descriptor.Descriptor( @@ -7241,8 +7262,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25122, - serialized_end=25582, + serialized_start=25222, + serialized_end=25682, ) _GETCONTESTEDRESOURCESREQUEST = _descriptor.Descriptor( @@ -7277,8 +7298,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=24987, - serialized_end=25593, + serialized_start=25087, + serialized_end=25693, ) @@ -7309,8 +7330,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=26035, - serialized_end=26095, + serialized_start=26135, + serialized_end=26195, ) _GETCONTESTEDRESOURCESRESPONSE_GETCONTESTEDRESOURCESRESPONSEV0 = _descriptor.Descriptor( @@ -7359,8 +7380,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25734, - serialized_end=26105, + serialized_start=25834, + serialized_end=26205, ) _GETCONTESTEDRESOURCESRESPONSE = _descriptor.Descriptor( @@ -7395,8 +7416,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25596, - serialized_end=26116, + serialized_start=25696, + serialized_end=26216, ) @@ -7434,8 +7455,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=26629, - serialized_end=26702, + serialized_start=26729, + serialized_end=26802, ) _GETVOTEPOLLSBYENDDATEREQUEST_GETVOTEPOLLSBYENDDATEREQUESTV0_ENDATTIMEINFO = _descriptor.Descriptor( @@ -7472,8 +7493,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=26704, - serialized_end=26771, + serialized_start=26804, + serialized_end=26871, ) _GETVOTEPOLLSBYENDDATEREQUEST_GETVOTEPOLLSBYENDDATEREQUESTV0 = _descriptor.Descriptor( @@ -7558,8 +7579,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26254, - serialized_end=26830, + serialized_start=26354, + serialized_end=26930, ) _GETVOTEPOLLSBYENDDATEREQUEST = _descriptor.Descriptor( @@ -7594,8 +7615,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26119, - serialized_end=26841, + serialized_start=26219, + serialized_end=26941, ) @@ -7633,8 +7654,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=27290, - serialized_end=27376, + serialized_start=27390, + serialized_end=27476, ) _GETVOTEPOLLSBYENDDATERESPONSE_GETVOTEPOLLSBYENDDATERESPONSEV0_SERIALIZEDVOTEPOLLSBYTIMESTAMPS = _descriptor.Descriptor( @@ -7671,8 +7692,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=27379, - serialized_end=27594, + serialized_start=27479, + serialized_end=27694, ) _GETVOTEPOLLSBYENDDATERESPONSE_GETVOTEPOLLSBYENDDATERESPONSEV0 = _descriptor.Descriptor( @@ -7721,8 +7742,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26982, - serialized_end=27604, + serialized_start=27082, + serialized_end=27704, ) _GETVOTEPOLLSBYENDDATERESPONSE = _descriptor.Descriptor( @@ -7757,8 +7778,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26844, - serialized_end=27615, + serialized_start=26944, + serialized_end=27715, ) @@ -7796,8 +7817,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=28304, - serialized_end=28388, + serialized_start=28404, + serialized_end=28488, ) _GETCONTESTEDRESOURCEVOTESTATEREQUEST_GETCONTESTEDRESOURCEVOTESTATEREQUESTV0 = _descriptor.Descriptor( @@ -7894,8 +7915,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=27777, - serialized_end=28502, + serialized_start=27877, + serialized_end=28602, ) _GETCONTESTEDRESOURCEVOTESTATEREQUEST = _descriptor.Descriptor( @@ -7930,8 +7951,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=27618, - serialized_end=28513, + serialized_start=27718, + serialized_end=28613, ) @@ -8003,8 +8024,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=29013, - serialized_end=29487, + serialized_start=29113, + serialized_end=29587, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0_CONTESTEDRESOURCECONTENDERS = _descriptor.Descriptor( @@ -8070,8 +8091,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=29490, - serialized_end=29942, + serialized_start=29590, + serialized_end=30042, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0_CONTENDER = _descriptor.Descriptor( @@ -8125,8 +8146,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=29944, - serialized_end=30051, + serialized_start=30044, + serialized_end=30151, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0 = _descriptor.Descriptor( @@ -8175,8 +8196,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=28678, - serialized_end=30061, + serialized_start=28778, + serialized_end=30161, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE = _descriptor.Descriptor( @@ -8211,8 +8232,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=28516, - serialized_end=30072, + serialized_start=28616, + serialized_end=30172, ) @@ -8250,8 +8271,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=28304, - serialized_end=28388, + serialized_start=28404, + serialized_end=28488, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYREQUEST_GETCONTESTEDRESOURCEVOTERSFORIDENTITYREQUESTV0 = _descriptor.Descriptor( @@ -8347,8 +8368,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30259, - serialized_end=30789, + serialized_start=30359, + serialized_end=30889, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYREQUEST = _descriptor.Descriptor( @@ -8383,8 +8404,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30075, - serialized_end=30800, + serialized_start=30175, + serialized_end=30900, ) @@ -8422,8 +8443,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=31340, - serialized_end=31407, + serialized_start=31440, + serialized_end=31507, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYRESPONSE_GETCONTESTEDRESOURCEVOTERSFORIDENTITYRESPONSEV0 = _descriptor.Descriptor( @@ -8472,8 +8493,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30990, - serialized_end=31417, + serialized_start=31090, + serialized_end=31517, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYRESPONSE = _descriptor.Descriptor( @@ -8508,8 +8529,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30803, - serialized_end=31428, + serialized_start=30903, + serialized_end=31528, ) @@ -8547,8 +8568,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=31977, - serialized_end=32074, + serialized_start=32077, + serialized_end=32174, ) _GETCONTESTEDRESOURCEIDENTITYVOTESREQUEST_GETCONTESTEDRESOURCEIDENTITYVOTESREQUESTV0 = _descriptor.Descriptor( @@ -8618,8 +8639,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=31602, - serialized_end=32105, + serialized_start=31702, + serialized_end=32205, ) _GETCONTESTEDRESOURCEIDENTITYVOTESREQUEST = _descriptor.Descriptor( @@ -8654,8 +8675,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=31431, - serialized_end=32116, + serialized_start=31531, + serialized_end=32216, ) @@ -8693,8 +8714,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=32619, - serialized_end=32866, + serialized_start=32719, + serialized_end=32966, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0_RESOURCEVOTECHOICE = _descriptor.Descriptor( @@ -8737,8 +8758,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=32869, - serialized_end=33170, + serialized_start=32969, + serialized_end=33270, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0_CONTESTEDRESOURCEIDENTITYVOTE = _descriptor.Descriptor( @@ -8789,8 +8810,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=33173, - serialized_end=33450, + serialized_start=33273, + serialized_end=33550, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0 = _descriptor.Descriptor( @@ -8839,8 +8860,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=32293, - serialized_end=33460, + serialized_start=32393, + serialized_end=33560, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE = _descriptor.Descriptor( @@ -8875,8 +8896,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=32119, - serialized_end=33471, + serialized_start=32219, + serialized_end=33571, ) @@ -8914,8 +8935,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=33635, - serialized_end=33703, + serialized_start=33735, + serialized_end=33803, ) _GETPREFUNDEDSPECIALIZEDBALANCEREQUEST = _descriptor.Descriptor( @@ -8950,8 +8971,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=33474, - serialized_end=33714, + serialized_start=33574, + serialized_end=33814, ) @@ -9001,8 +9022,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=33882, - serialized_end=34071, + serialized_start=33982, + serialized_end=34171, ) _GETPREFUNDEDSPECIALIZEDBALANCERESPONSE = _descriptor.Descriptor( @@ -9037,8 +9058,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=33717, - serialized_end=34082, + serialized_start=33817, + serialized_end=34182, ) @@ -9069,8 +9090,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=34231, - serialized_end=34282, + serialized_start=34331, + serialized_end=34382, ) _GETTOTALCREDITSINPLATFORMREQUEST = _descriptor.Descriptor( @@ -9105,8 +9126,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34085, - serialized_end=34293, + serialized_start=34185, + serialized_end=34393, ) @@ -9156,8 +9177,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34446, - serialized_end=34630, + serialized_start=34546, + serialized_end=34730, ) _GETTOTALCREDITSINPLATFORMRESPONSE = _descriptor.Descriptor( @@ -9192,8 +9213,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34296, - serialized_end=34641, + serialized_start=34396, + serialized_end=34741, ) @@ -9238,8 +9259,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=34760, - serialized_end=34829, + serialized_start=34860, + serialized_end=34929, ) _GETPATHELEMENTSREQUEST = _descriptor.Descriptor( @@ -9274,8 +9295,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34644, - serialized_end=34840, + serialized_start=34744, + serialized_end=34940, ) @@ -9306,8 +9327,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=35213, - serialized_end=35241, + serialized_start=35313, + serialized_end=35341, ) _GETPATHELEMENTSRESPONSE_GETPATHELEMENTSRESPONSEV0 = _descriptor.Descriptor( @@ -9356,8 +9377,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34963, - serialized_end=35251, + serialized_start=35063, + serialized_end=35351, ) _GETPATHELEMENTSRESPONSE = _descriptor.Descriptor( @@ -9392,8 +9413,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34843, - serialized_end=35262, + serialized_start=34943, + serialized_end=35362, ) @@ -9417,8 +9438,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=35363, - serialized_end=35383, + serialized_start=35463, + serialized_end=35483, ) _GETSTATUSREQUEST = _descriptor.Descriptor( @@ -9453,8 +9474,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=35265, - serialized_end=35394, + serialized_start=35365, + serialized_end=35494, ) @@ -9509,8 +9530,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36271, - serialized_end=36365, + serialized_start=36371, + serialized_end=36465, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION_PROTOCOL_TENDERDASH = _descriptor.Descriptor( @@ -9547,8 +9568,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36598, - serialized_end=36638, + serialized_start=36698, + serialized_end=36738, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION_PROTOCOL_DRIVE = _descriptor.Descriptor( @@ -9592,8 +9613,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36640, - serialized_end=36700, + serialized_start=36740, + serialized_end=36800, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION_PROTOCOL = _descriptor.Descriptor( @@ -9630,8 +9651,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36368, - serialized_end=36700, + serialized_start=36468, + serialized_end=36800, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION = _descriptor.Descriptor( @@ -9668,8 +9689,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36058, - serialized_end=36700, + serialized_start=36158, + serialized_end=36800, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_TIME = _descriptor.Descriptor( @@ -9735,8 +9756,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36702, - serialized_end=36829, + serialized_start=36802, + serialized_end=36929, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_NODE = _descriptor.Descriptor( @@ -9778,8 +9799,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36831, - serialized_end=36891, + serialized_start=36931, + serialized_end=36991, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_CHAIN = _descriptor.Descriptor( @@ -9870,8 +9891,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36894, - serialized_end=37201, + serialized_start=36994, + serialized_end=37301, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_NETWORK = _descriptor.Descriptor( @@ -9915,8 +9936,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37203, - serialized_end=37270, + serialized_start=37303, + serialized_end=37370, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_STATESYNC = _descriptor.Descriptor( @@ -9995,8 +10016,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37273, - serialized_end=37534, + serialized_start=37373, + serialized_end=37634, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0 = _descriptor.Descriptor( @@ -10061,8 +10082,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=35499, - serialized_end=37534, + serialized_start=35599, + serialized_end=37634, ) _GETSTATUSRESPONSE = _descriptor.Descriptor( @@ -10097,8 +10118,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=35397, - serialized_end=37545, + serialized_start=35497, + serialized_end=37645, ) @@ -10122,8 +10143,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37682, - serialized_end=37714, + serialized_start=37782, + serialized_end=37814, ) _GETCURRENTQUORUMSINFOREQUEST = _descriptor.Descriptor( @@ -10158,8 +10179,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=37548, - serialized_end=37725, + serialized_start=37648, + serialized_end=37825, ) @@ -10204,8 +10225,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37865, - serialized_end=37935, + serialized_start=37965, + serialized_end=38035, ) _GETCURRENTQUORUMSINFORESPONSE_VALIDATORSETV0 = _descriptor.Descriptor( @@ -10256,8 +10277,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37938, - serialized_end=38113, + serialized_start=38038, + serialized_end=38213, ) _GETCURRENTQUORUMSINFORESPONSE_GETCURRENTQUORUMSINFORESPONSEV0 = _descriptor.Descriptor( @@ -10315,8 +10336,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=38116, - serialized_end=38390, + serialized_start=38216, + serialized_end=38490, ) _GETCURRENTQUORUMSINFORESPONSE = _descriptor.Descriptor( @@ -10351,8 +10372,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=37728, - serialized_end=38401, + serialized_start=37828, + serialized_end=38501, ) @@ -10397,8 +10418,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=38547, - serialized_end=38637, + serialized_start=38647, + serialized_end=38737, ) _GETIDENTITYTOKENBALANCESREQUEST = _descriptor.Descriptor( @@ -10433,8 +10454,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=38404, - serialized_end=38648, + serialized_start=38504, + serialized_end=38748, ) @@ -10477,8 +10498,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39087, - serialized_end=39158, + serialized_start=39187, + serialized_end=39258, ) _GETIDENTITYTOKENBALANCESRESPONSE_GETIDENTITYTOKENBALANCESRESPONSEV0_TOKENBALANCES = _descriptor.Descriptor( @@ -10508,8 +10529,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=39161, - serialized_end=39315, + serialized_start=39261, + serialized_end=39415, ) _GETIDENTITYTOKENBALANCESRESPONSE_GETIDENTITYTOKENBALANCESRESPONSEV0 = _descriptor.Descriptor( @@ -10558,8 +10579,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=38798, - serialized_end=39325, + serialized_start=38898, + serialized_end=39425, ) _GETIDENTITYTOKENBALANCESRESPONSE = _descriptor.Descriptor( @@ -10594,8 +10615,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=38651, - serialized_end=39336, + serialized_start=38751, + serialized_end=39436, ) @@ -10640,8 +10661,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=39488, - serialized_end=39580, + serialized_start=39588, + serialized_end=39680, ) _GETIDENTITIESTOKENBALANCESREQUEST = _descriptor.Descriptor( @@ -10676,8 +10697,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39339, - serialized_end=39591, + serialized_start=39439, + serialized_end=39691, ) @@ -10720,8 +10741,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40059, - serialized_end=40141, + serialized_start=40159, + serialized_end=40241, ) _GETIDENTITIESTOKENBALANCESRESPONSE_GETIDENTITIESTOKENBALANCESRESPONSEV0_IDENTITYTOKENBALANCES = _descriptor.Descriptor( @@ -10751,8 +10772,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=40144, - serialized_end=40327, + serialized_start=40244, + serialized_end=40427, ) _GETIDENTITIESTOKENBALANCESRESPONSE_GETIDENTITIESTOKENBALANCESRESPONSEV0 = _descriptor.Descriptor( @@ -10801,8 +10822,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39747, - serialized_end=40337, + serialized_start=39847, + serialized_end=40437, ) _GETIDENTITIESTOKENBALANCESRESPONSE = _descriptor.Descriptor( @@ -10837,8 +10858,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39594, - serialized_end=40348, + serialized_start=39694, + serialized_end=40448, ) @@ -10883,8 +10904,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=40485, - serialized_end=40572, + serialized_start=40585, + serialized_end=40672, ) _GETIDENTITYTOKENINFOSREQUEST = _descriptor.Descriptor( @@ -10919,8 +10940,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40351, - serialized_end=40583, + serialized_start=40451, + serialized_end=40683, ) @@ -10951,8 +10972,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=40997, - serialized_end=41037, + serialized_start=41097, + serialized_end=41137, ) _GETIDENTITYTOKENINFOSRESPONSE_GETIDENTITYTOKENINFOSRESPONSEV0_TOKENINFOENTRY = _descriptor.Descriptor( @@ -10994,8 +11015,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41040, - serialized_end=41216, + serialized_start=41140, + serialized_end=41316, ) _GETIDENTITYTOKENINFOSRESPONSE_GETIDENTITYTOKENINFOSRESPONSEV0_TOKENINFOS = _descriptor.Descriptor( @@ -11025,8 +11046,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=41219, - serialized_end=41357, + serialized_start=41319, + serialized_end=41457, ) _GETIDENTITYTOKENINFOSRESPONSE_GETIDENTITYTOKENINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -11075,8 +11096,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40724, - serialized_end=41367, + serialized_start=40824, + serialized_end=41467, ) _GETIDENTITYTOKENINFOSRESPONSE = _descriptor.Descriptor( @@ -11111,8 +11132,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40586, - serialized_end=41378, + serialized_start=40686, + serialized_end=41478, ) @@ -11157,8 +11178,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=41521, - serialized_end=41610, + serialized_start=41621, + serialized_end=41710, ) _GETIDENTITIESTOKENINFOSREQUEST = _descriptor.Descriptor( @@ -11193,8 +11214,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41381, - serialized_end=41621, + serialized_start=41481, + serialized_end=41721, ) @@ -11225,8 +11246,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=40997, - serialized_end=41037, + serialized_start=41097, + serialized_end=41137, ) _GETIDENTITIESTOKENINFOSRESPONSE_GETIDENTITIESTOKENINFOSRESPONSEV0_TOKENINFOENTRY = _descriptor.Descriptor( @@ -11268,8 +11289,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42108, - serialized_end=42291, + serialized_start=42208, + serialized_end=42391, ) _GETIDENTITIESTOKENINFOSRESPONSE_GETIDENTITIESTOKENINFOSRESPONSEV0_IDENTITYTOKENINFOS = _descriptor.Descriptor( @@ -11299,8 +11320,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=42294, - serialized_end=42445, + serialized_start=42394, + serialized_end=42545, ) _GETIDENTITIESTOKENINFOSRESPONSE_GETIDENTITIESTOKENINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -11349,8 +11370,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41768, - serialized_end=42455, + serialized_start=41868, + serialized_end=42555, ) _GETIDENTITIESTOKENINFOSRESPONSE = _descriptor.Descriptor( @@ -11385,8 +11406,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41624, - serialized_end=42466, + serialized_start=41724, + serialized_end=42566, ) @@ -11424,8 +11445,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=42588, - serialized_end=42649, + serialized_start=42688, + serialized_end=42749, ) _GETTOKENSTATUSESREQUEST = _descriptor.Descriptor( @@ -11460,8 +11481,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42469, - serialized_end=42660, + serialized_start=42569, + serialized_end=42760, ) @@ -11504,8 +11525,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43050, - serialized_end=43118, + serialized_start=43150, + serialized_end=43218, ) _GETTOKENSTATUSESRESPONSE_GETTOKENSTATUSESRESPONSEV0_TOKENSTATUSES = _descriptor.Descriptor( @@ -11535,8 +11556,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=43121, - serialized_end=43257, + serialized_start=43221, + serialized_end=43357, ) _GETTOKENSTATUSESRESPONSE_GETTOKENSTATUSESRESPONSEV0 = _descriptor.Descriptor( @@ -11585,8 +11606,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42786, - serialized_end=43267, + serialized_start=42886, + serialized_end=43367, ) _GETTOKENSTATUSESRESPONSE = _descriptor.Descriptor( @@ -11621,8 +11642,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42663, - serialized_end=43278, + serialized_start=42763, + serialized_end=43378, ) @@ -11660,8 +11681,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=43436, - serialized_end=43509, + serialized_start=43536, + serialized_end=43609, ) _GETTOKENDIRECTPURCHASEPRICESREQUEST = _descriptor.Descriptor( @@ -11696,8 +11717,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43281, - serialized_end=43520, + serialized_start=43381, + serialized_end=43620, ) @@ -11735,8 +11756,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44010, - serialized_end=44061, + serialized_start=44110, + serialized_end=44161, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0_PRICINGSCHEDULE = _descriptor.Descriptor( @@ -11766,8 +11787,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44064, - serialized_end=44231, + serialized_start=44164, + serialized_end=44331, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0_TOKENDIRECTPURCHASEPRICEENTRY = _descriptor.Descriptor( @@ -11816,8 +11837,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=44234, - serialized_end=44462, + serialized_start=44334, + serialized_end=44562, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0_TOKENDIRECTPURCHASEPRICES = _descriptor.Descriptor( @@ -11847,8 +11868,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44465, - serialized_end=44665, + serialized_start=44565, + serialized_end=44765, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0 = _descriptor.Descriptor( @@ -11897,8 +11918,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43682, - serialized_end=44675, + serialized_start=43782, + serialized_end=44775, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE = _descriptor.Descriptor( @@ -11933,8 +11954,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43523, - serialized_end=44686, + serialized_start=43623, + serialized_end=44786, ) @@ -11972,8 +11993,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44820, - serialized_end=44884, + serialized_start=44920, + serialized_end=44984, ) _GETTOKENCONTRACTINFOREQUEST = _descriptor.Descriptor( @@ -12008,8 +12029,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=44689, - serialized_end=44895, + serialized_start=44789, + serialized_end=44995, ) @@ -12047,8 +12068,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=45307, - serialized_end=45384, + serialized_start=45407, + serialized_end=45484, ) _GETTOKENCONTRACTINFORESPONSE_GETTOKENCONTRACTINFORESPONSEV0 = _descriptor.Descriptor( @@ -12097,8 +12118,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45033, - serialized_end=45394, + serialized_start=45133, + serialized_end=45494, ) _GETTOKENCONTRACTINFORESPONSE = _descriptor.Descriptor( @@ -12133,8 +12154,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=44898, - serialized_end=45405, + serialized_start=44998, + serialized_end=45505, ) @@ -12189,8 +12210,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45838, - serialized_end=45992, + serialized_start=45938, + serialized_end=46092, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSREQUEST_GETTOKENPREPROGRAMMEDDISTRIBUTIONSREQUESTV0 = _descriptor.Descriptor( @@ -12251,8 +12272,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45582, - serialized_end=46020, + serialized_start=45682, + serialized_end=46120, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSREQUEST = _descriptor.Descriptor( @@ -12287,8 +12308,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45408, - serialized_end=46031, + serialized_start=45508, + serialized_end=46131, ) @@ -12326,8 +12347,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=46542, - serialized_end=46604, + serialized_start=46642, + serialized_end=46704, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE_GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSEV0_TOKENTIMEDDISTRIBUTIONENTRY = _descriptor.Descriptor( @@ -12364,8 +12385,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=46607, - serialized_end=46819, + serialized_start=46707, + serialized_end=46919, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE_GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSEV0_TOKENDISTRIBUTIONS = _descriptor.Descriptor( @@ -12395,8 +12416,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=46822, - serialized_end=47017, + serialized_start=46922, + serialized_end=47117, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE_GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSEV0 = _descriptor.Descriptor( @@ -12445,8 +12466,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=46212, - serialized_end=47027, + serialized_start=46312, + serialized_end=47127, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE = _descriptor.Descriptor( @@ -12481,8 +12502,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=46034, - serialized_end=47038, + serialized_start=46134, + serialized_end=47138, ) @@ -12520,8 +12541,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=47227, - serialized_end=47300, + serialized_start=47327, + serialized_end=47400, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMREQUEST_GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMREQUESTV0 = _descriptor.Descriptor( @@ -12577,8 +12598,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47303, - serialized_end=47544, + serialized_start=47403, + serialized_end=47644, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMREQUEST = _descriptor.Descriptor( @@ -12613,8 +12634,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47041, - serialized_end=47555, + serialized_start=47141, + serialized_end=47655, ) @@ -12671,8 +12692,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48076, - serialized_end=48196, + serialized_start=48176, + serialized_end=48296, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMRESPONSE_GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMRESPONSEV0 = _descriptor.Descriptor( @@ -12721,8 +12742,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47748, - serialized_end=48206, + serialized_start=47848, + serialized_end=48306, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMRESPONSE = _descriptor.Descriptor( @@ -12757,8 +12778,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47558, - serialized_end=48217, + serialized_start=47658, + serialized_end=48317, ) @@ -12796,8 +12817,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=48348, - serialized_end=48411, + serialized_start=48448, + serialized_end=48511, ) _GETTOKENTOTALSUPPLYREQUEST = _descriptor.Descriptor( @@ -12832,8 +12853,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48220, - serialized_end=48422, + serialized_start=48320, + serialized_end=48522, ) @@ -12878,8 +12899,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=48843, - serialized_end=48963, + serialized_start=48943, + serialized_end=49063, ) _GETTOKENTOTALSUPPLYRESPONSE_GETTOKENTOTALSUPPLYRESPONSEV0 = _descriptor.Descriptor( @@ -12928,8 +12949,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48557, - serialized_end=48973, + serialized_start=48657, + serialized_end=49073, ) _GETTOKENTOTALSUPPLYRESPONSE = _descriptor.Descriptor( @@ -12964,8 +12985,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48425, - serialized_end=48984, + serialized_start=48525, + serialized_end=49084, ) @@ -13010,8 +13031,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49094, - serialized_end=49186, + serialized_start=49194, + serialized_end=49286, ) _GETGROUPINFOREQUEST = _descriptor.Descriptor( @@ -13046,8 +13067,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48987, - serialized_end=49197, + serialized_start=49087, + serialized_end=49297, ) @@ -13085,8 +13106,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49555, - serialized_end=49607, + serialized_start=49655, + serialized_end=49707, ) _GETGROUPINFORESPONSE_GETGROUPINFORESPONSEV0_GROUPINFOENTRY = _descriptor.Descriptor( @@ -13123,8 +13144,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49610, - serialized_end=49762, + serialized_start=49710, + serialized_end=49862, ) _GETGROUPINFORESPONSE_GETGROUPINFORESPONSEV0_GROUPINFO = _descriptor.Descriptor( @@ -13159,8 +13180,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49765, - serialized_end=49903, + serialized_start=49865, + serialized_end=50003, ) _GETGROUPINFORESPONSE_GETGROUPINFORESPONSEV0 = _descriptor.Descriptor( @@ -13209,8 +13230,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49311, - serialized_end=49913, + serialized_start=49411, + serialized_end=50013, ) _GETGROUPINFORESPONSE = _descriptor.Descriptor( @@ -13245,8 +13266,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49200, - serialized_end=49924, + serialized_start=49300, + serialized_end=50024, ) @@ -13284,8 +13305,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=50037, - serialized_end=50154, + serialized_start=50137, + serialized_end=50254, ) _GETGROUPINFOSREQUEST_GETGROUPINFOSREQUESTV0 = _descriptor.Descriptor( @@ -13346,8 +13367,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50157, - serialized_end=50409, + serialized_start=50257, + serialized_end=50509, ) _GETGROUPINFOSREQUEST = _descriptor.Descriptor( @@ -13382,8 +13403,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49927, - serialized_end=50420, + serialized_start=50027, + serialized_end=50520, ) @@ -13421,8 +13442,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49555, - serialized_end=49607, + serialized_start=49655, + serialized_end=49707, ) _GETGROUPINFOSRESPONSE_GETGROUPINFOSRESPONSEV0_GROUPPOSITIONINFOENTRY = _descriptor.Descriptor( @@ -13466,8 +13487,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=50841, - serialized_end=51036, + serialized_start=50941, + serialized_end=51136, ) _GETGROUPINFOSRESPONSE_GETGROUPINFOSRESPONSEV0_GROUPINFOS = _descriptor.Descriptor( @@ -13497,8 +13518,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=51039, - serialized_end=51169, + serialized_start=51139, + serialized_end=51269, ) _GETGROUPINFOSRESPONSE_GETGROUPINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -13547,8 +13568,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50537, - serialized_end=51179, + serialized_start=50637, + serialized_end=51279, ) _GETGROUPINFOSRESPONSE = _descriptor.Descriptor( @@ -13583,8 +13604,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50423, - serialized_end=51190, + serialized_start=50523, + serialized_end=51290, ) @@ -13622,8 +13643,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=51309, - serialized_end=51385, + serialized_start=51409, + serialized_end=51485, ) _GETGROUPACTIONSREQUEST_GETGROUPACTIONSREQUESTV0 = _descriptor.Descriptor( @@ -13698,8 +13719,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51388, - serialized_end=51716, + serialized_start=51488, + serialized_end=51816, ) _GETGROUPACTIONSREQUEST = _descriptor.Descriptor( @@ -13735,8 +13756,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51193, - serialized_end=51767, + serialized_start=51293, + serialized_end=51867, ) @@ -13786,8 +13807,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52149, - serialized_end=52240, + serialized_start=52249, + serialized_end=52340, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNEVENT = _descriptor.Descriptor( @@ -13836,8 +13857,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52242, - serialized_end=52333, + serialized_start=52342, + serialized_end=52433, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_FREEZEEVENT = _descriptor.Descriptor( @@ -13879,8 +13900,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52335, - serialized_end=52409, + serialized_start=52435, + serialized_end=52509, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UNFREEZEEVENT = _descriptor.Descriptor( @@ -13922,8 +13943,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52411, - serialized_end=52487, + serialized_start=52511, + serialized_end=52587, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DESTROYFROZENFUNDSEVENT = _descriptor.Descriptor( @@ -13972,8 +13993,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52489, - serialized_end=52591, + serialized_start=52589, + serialized_end=52691, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_SHAREDENCRYPTEDNOTE = _descriptor.Descriptor( @@ -14017,8 +14038,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=52593, - serialized_end=52693, + serialized_start=52693, + serialized_end=52793, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_PERSONALENCRYPTEDNOTE = _descriptor.Descriptor( @@ -14062,8 +14083,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=52695, - serialized_end=52818, + serialized_start=52795, + serialized_end=52918, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT = _descriptor.Descriptor( @@ -14106,8 +14127,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52821, - serialized_end=53054, + serialized_start=52921, + serialized_end=53154, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT = _descriptor.Descriptor( @@ -14149,8 +14170,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=53056, - serialized_end=53156, + serialized_start=53156, + serialized_end=53256, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT_PRICEFORQUANTITY = _descriptor.Descriptor( @@ -14187,8 +14208,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44010, - serialized_end=44061, + serialized_start=44110, + serialized_end=44161, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT_PRICINGSCHEDULE = _descriptor.Descriptor( @@ -14218,8 +14239,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=53448, - serialized_end=53620, + serialized_start=53548, + serialized_end=53720, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT = _descriptor.Descriptor( @@ -14273,8 +14294,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=53159, - serialized_end=53645, + serialized_start=53259, + serialized_end=53745, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT = _descriptor.Descriptor( @@ -14323,8 +14344,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=53648, - serialized_end=54028, + serialized_start=53748, + serialized_end=54128, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT = _descriptor.Descriptor( @@ -14359,8 +14380,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=54031, - serialized_end=54170, + serialized_start=54131, + serialized_end=54270, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT = _descriptor.Descriptor( @@ -14390,8 +14411,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=54172, - serialized_end=54219, + serialized_start=54272, + serialized_end=54319, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT = _descriptor.Descriptor( @@ -14421,8 +14442,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=54221, - serialized_end=54268, + serialized_start=54321, + serialized_end=54368, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT = _descriptor.Descriptor( @@ -14457,8 +14478,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=54271, - serialized_end=54410, + serialized_start=54371, + serialized_end=54510, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT = _descriptor.Descriptor( @@ -14542,8 +14563,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=54413, - serialized_end=55390, + serialized_start=54513, + serialized_end=55490, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY = _descriptor.Descriptor( @@ -14580,8 +14601,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=55393, - serialized_end=55540, + serialized_start=55493, + serialized_end=55640, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS = _descriptor.Descriptor( @@ -14611,8 +14632,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=55543, - serialized_end=55675, + serialized_start=55643, + serialized_end=55775, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 = _descriptor.Descriptor( @@ -14661,8 +14682,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51890, - serialized_end=55685, + serialized_start=51990, + serialized_end=55785, ) _GETGROUPACTIONSRESPONSE = _descriptor.Descriptor( @@ -14697,8 +14718,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51770, - serialized_end=55696, + serialized_start=51870, + serialized_end=55796, ) @@ -14757,8 +14778,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=55834, - serialized_end=56040, + serialized_start=55934, + serialized_end=56140, ) _GETGROUPACTIONSIGNERSREQUEST = _descriptor.Descriptor( @@ -14794,8 +14815,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=55699, - serialized_end=56091, + serialized_start=55799, + serialized_end=56191, ) @@ -14833,8 +14854,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=56523, - serialized_end=56576, + serialized_start=56623, + serialized_end=56676, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0_GROUPACTIONSIGNERS = _descriptor.Descriptor( @@ -14864,8 +14885,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=56579, - serialized_end=56724, + serialized_start=56679, + serialized_end=56824, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0 = _descriptor.Descriptor( @@ -14914,8 +14935,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56232, - serialized_end=56734, + serialized_start=56332, + serialized_end=56834, ) _GETGROUPACTIONSIGNERSRESPONSE = _descriptor.Descriptor( @@ -14950,8 +14971,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56094, - serialized_end=56745, + serialized_start=56194, + serialized_end=56845, ) @@ -14989,8 +15010,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=56861, - serialized_end=56918, + serialized_start=56961, + serialized_end=57018, ) _GETADDRESSINFOREQUEST = _descriptor.Descriptor( @@ -15025,8 +15046,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56748, - serialized_end=56929, + serialized_start=56848, + serialized_end=57029, ) @@ -15069,8 +15090,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56932, - serialized_end=57065, + serialized_start=57032, + serialized_end=57165, ) @@ -15108,8 +15129,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57067, - serialized_end=57116, + serialized_start=57167, + serialized_end=57216, ) @@ -15140,8 +15161,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57118, - serialized_end=57213, + serialized_start=57218, + serialized_end=57313, ) @@ -15191,8 +15212,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57215, - serialized_end=57324, + serialized_start=57315, + serialized_end=57424, ) @@ -15230,8 +15251,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57326, - serialized_end=57446, + serialized_start=57426, + serialized_end=57546, ) @@ -15262,8 +15283,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57448, - serialized_end=57555, + serialized_start=57548, + serialized_end=57655, ) @@ -15313,8 +15334,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57675, - serialized_end=57900, + serialized_start=57775, + serialized_end=58000, ) _GETADDRESSINFORESPONSE = _descriptor.Descriptor( @@ -15349,8 +15370,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57558, - serialized_end=57911, + serialized_start=57658, + serialized_end=58011, ) @@ -15388,8 +15409,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=58036, - serialized_end=58098, + serialized_start=58136, + serialized_end=58198, ) _GETADDRESSESINFOSREQUEST = _descriptor.Descriptor( @@ -15424,8 +15445,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57914, - serialized_end=58109, + serialized_start=58014, + serialized_end=58209, ) @@ -15475,8 +15496,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58238, - serialized_end=58470, + serialized_start=58338, + serialized_end=58570, ) _GETADDRESSESINFOSRESPONSE = _descriptor.Descriptor( @@ -15511,8 +15532,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58112, - serialized_end=58481, + serialized_start=58212, + serialized_end=58581, ) @@ -15536,8 +15557,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=58621, - serialized_end=58654, + serialized_start=58721, + serialized_end=58754, ) _GETADDRESSESTRUNKSTATEREQUEST = _descriptor.Descriptor( @@ -15572,8 +15593,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58484, - serialized_end=58665, + serialized_start=58584, + serialized_end=58765, ) @@ -15611,8 +15632,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=58809, - serialized_end=58955, + serialized_start=58909, + serialized_end=59055, ) _GETADDRESSESTRUNKSTATERESPONSE = _descriptor.Descriptor( @@ -15647,8 +15668,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58668, - serialized_end=58966, + serialized_start=58768, + serialized_end=59066, ) @@ -15693,8 +15714,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=59109, - serialized_end=59198, + serialized_start=59209, + serialized_end=59298, ) _GETADDRESSESBRANCHSTATEREQUEST = _descriptor.Descriptor( @@ -15729,8 +15750,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58969, - serialized_end=59209, + serialized_start=59069, + serialized_end=59309, ) @@ -15761,8 +15782,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=59355, - serialized_end=59410, + serialized_start=59455, + serialized_end=59510, ) _GETADDRESSESBRANCHSTATERESPONSE = _descriptor.Descriptor( @@ -15797,8 +15818,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59212, - serialized_end=59421, + serialized_start=59312, + serialized_end=59521, ) @@ -15843,8 +15864,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=59585, - serialized_end=59699, + serialized_start=59685, + serialized_end=59799, ) _GETRECENTADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -15879,8 +15900,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59424, - serialized_end=59710, + serialized_start=59524, + serialized_end=59810, ) @@ -15930,8 +15951,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59878, - serialized_end=60142, + serialized_start=59978, + serialized_end=60242, ) _GETRECENTADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -15966,8 +15987,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59713, - serialized_end=60153, + serialized_start=59813, + serialized_end=60253, ) @@ -16005,8 +16026,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60155, - serialized_end=60226, + serialized_start=60255, + serialized_end=60326, ) @@ -16056,8 +16077,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=60229, - serialized_end=60405, + serialized_start=60329, + serialized_end=60505, ) @@ -16088,8 +16109,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60407, - serialized_end=60499, + serialized_start=60507, + serialized_end=60599, ) @@ -16134,8 +16155,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60502, - serialized_end=60676, + serialized_start=60602, + serialized_end=60776, ) @@ -16166,8 +16187,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60679, - serialized_end=60814, + serialized_start=60779, + serialized_end=60914, ) @@ -16205,8 +16226,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=61006, - serialized_end=61103, + serialized_start=61106, + serialized_end=61203, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -16241,8 +16262,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=60817, - serialized_end=61114, + serialized_start=60917, + serialized_end=61214, ) @@ -16292,8 +16313,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61310, - serialized_end=61602, + serialized_start=61410, + serialized_end=61702, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -16328,8 +16349,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61117, - serialized_end=61613, + serialized_start=61217, + serialized_end=61713, ) @@ -16374,8 +16395,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=61762, - serialized_end=61849, + serialized_start=61862, + serialized_end=61949, ) _GETSHIELDEDENCRYPTEDNOTESREQUEST = _descriptor.Descriptor( @@ -16410,8 +16431,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61616, - serialized_end=61860, + serialized_start=61716, + serialized_end=61960, ) @@ -16463,8 +16484,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=62307, - serialized_end=62394, + serialized_start=62407, + serialized_end=62494, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0_ENCRYPTEDNOTES = _descriptor.Descriptor( @@ -16494,8 +16515,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=62397, - serialized_end=62542, + serialized_start=62497, + serialized_end=62642, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0 = _descriptor.Descriptor( @@ -16544,8 +16565,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62013, - serialized_end=62552, + serialized_start=62113, + serialized_end=62652, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE = _descriptor.Descriptor( @@ -16580,8 +16601,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61863, - serialized_end=62563, + serialized_start=61963, + serialized_end=62663, ) @@ -16612,8 +16633,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=62691, - serialized_end=62735, + serialized_start=62791, + serialized_end=62835, ) _GETSHIELDEDANCHORSREQUEST = _descriptor.Descriptor( @@ -16648,8 +16669,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62566, - serialized_end=62746, + serialized_start=62666, + serialized_end=62846, ) @@ -16680,8 +16701,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=63135, - serialized_end=63161, + serialized_start=63235, + serialized_end=63261, ) _GETSHIELDEDANCHORSRESPONSE_GETSHIELDEDANCHORSRESPONSEV0 = _descriptor.Descriptor( @@ -16730,8 +16751,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62878, - serialized_end=63171, + serialized_start=62978, + serialized_end=63271, ) _GETSHIELDEDANCHORSRESPONSE = _descriptor.Descriptor( @@ -16766,8 +16787,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62749, - serialized_end=63182, + serialized_start=62849, + serialized_end=63282, ) @@ -16798,8 +16819,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=63337, - serialized_end=63390, + serialized_start=63437, + serialized_end=63490, ) _GETMOSTRECENTSHIELDEDANCHORREQUEST = _descriptor.Descriptor( @@ -16834,8 +16855,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63185, - serialized_end=63401, + serialized_start=63285, + serialized_end=63501, ) @@ -16885,8 +16906,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63560, - serialized_end=63741, + serialized_start=63660, + serialized_end=63841, ) _GETMOSTRECENTSHIELDEDANCHORRESPONSE = _descriptor.Descriptor( @@ -16921,8 +16942,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63404, - serialized_end=63752, + serialized_start=63504, + serialized_end=63852, ) @@ -16953,8 +16974,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=63886, - serialized_end=63932, + serialized_start=63986, + serialized_end=64032, ) _GETSHIELDEDPOOLSTATEREQUEST = _descriptor.Descriptor( @@ -16989,8 +17010,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63755, - serialized_end=63943, + serialized_start=63855, + serialized_end=64043, ) @@ -17040,8 +17061,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64081, - serialized_end=64266, + serialized_start=64181, + serialized_end=64366, ) _GETSHIELDEDPOOLSTATERESPONSE = _descriptor.Descriptor( @@ -17076,8 +17097,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63946, - serialized_end=64277, + serialized_start=64046, + serialized_end=64377, ) @@ -17108,8 +17129,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=64414, - serialized_end=64461, + serialized_start=64514, + serialized_end=64561, ) _GETSHIELDEDNOTESCOUNTREQUEST = _descriptor.Descriptor( @@ -17144,8 +17165,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64280, - serialized_end=64472, + serialized_start=64380, + serialized_end=64572, ) @@ -17195,8 +17216,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64613, - serialized_end=64803, + serialized_start=64713, + serialized_end=64903, ) _GETSHIELDEDNOTESCOUNTRESPONSE = _descriptor.Descriptor( @@ -17231,8 +17252,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64475, - serialized_end=64814, + serialized_start=64575, + serialized_end=64914, ) @@ -17270,8 +17291,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=64951, - serialized_end=65018, + serialized_start=65051, + serialized_end=65118, ) _GETSHIELDEDNULLIFIERSREQUEST = _descriptor.Descriptor( @@ -17306,8 +17327,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64817, - serialized_end=65029, + serialized_start=64917, + serialized_end=65129, ) @@ -17345,8 +17366,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=65458, - serialized_end=65512, + serialized_start=65558, + serialized_end=65612, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0_NULLIFIERSTATUSES = _descriptor.Descriptor( @@ -17376,8 +17397,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=65515, - serialized_end=65657, + serialized_start=65615, + serialized_end=65757, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0 = _descriptor.Descriptor( @@ -17426,8 +17447,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=65170, - serialized_end=65667, + serialized_start=65270, + serialized_end=65767, ) _GETSHIELDEDNULLIFIERSRESPONSE = _descriptor.Descriptor( @@ -17462,8 +17483,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=65032, - serialized_end=65678, + serialized_start=65132, + serialized_end=65778, ) _GETIDENTITYREQUEST_GETIDENTITYREQUESTV0.containing_type = _GETIDENTITYREQUEST @@ -17795,24 +17816,14 @@ _GETDOCUMENTSREQUEST_HAVINGAGGREGATE.fields_by_name['function'].enum_type = _GETDOCUMENTSREQUEST_HAVINGAGGREGATE_FUNCTION _GETDOCUMENTSREQUEST_HAVINGAGGREGATE.containing_type = _GETDOCUMENTSREQUEST _GETDOCUMENTSREQUEST_HAVINGAGGREGATE_FUNCTION.containing_type = _GETDOCUMENTSREQUEST_HAVINGAGGREGATE -_GETDOCUMENTSREQUEST_HAVINGRANKING.fields_by_name['kind'].enum_type = _GETDOCUMENTSREQUEST_HAVINGRANKING_KIND -_GETDOCUMENTSREQUEST_HAVINGRANKING.containing_type = _GETDOCUMENTSREQUEST -_GETDOCUMENTSREQUEST_HAVINGRANKING_KIND.containing_type = _GETDOCUMENTSREQUEST_HAVINGRANKING -_GETDOCUMENTSREQUEST_HAVINGRANKING.oneofs_by_name['_n'].fields.append( - _GETDOCUMENTSREQUEST_HAVINGRANKING.fields_by_name['n']) -_GETDOCUMENTSREQUEST_HAVINGRANKING.fields_by_name['n'].containing_oneof = _GETDOCUMENTSREQUEST_HAVINGRANKING.oneofs_by_name['_n'] _GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['aggregate'].message_type = _GETDOCUMENTSREQUEST_HAVINGAGGREGATE _GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['operator'].enum_type = _GETDOCUMENTSREQUEST_HAVINGCLAUSE_OPERATOR _GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['value'].message_type = _GETDOCUMENTSREQUEST_DOCUMENTFIELDVALUE -_GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['ranking'].message_type = _GETDOCUMENTSREQUEST_HAVINGRANKING _GETDOCUMENTSREQUEST_HAVINGCLAUSE.containing_type = _GETDOCUMENTSREQUEST _GETDOCUMENTSREQUEST_HAVINGCLAUSE_OPERATOR.containing_type = _GETDOCUMENTSREQUEST_HAVINGCLAUSE _GETDOCUMENTSREQUEST_HAVINGCLAUSE.oneofs_by_name['right'].fields.append( _GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['value']) _GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['value'].containing_oneof = _GETDOCUMENTSREQUEST_HAVINGCLAUSE.oneofs_by_name['right'] -_GETDOCUMENTSREQUEST_HAVINGCLAUSE.oneofs_by_name['right'].fields.append( - _GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['ranking']) -_GETDOCUMENTSREQUEST_HAVINGCLAUSE.fields_by_name['ranking'].containing_oneof = _GETDOCUMENTSREQUEST_HAVINGCLAUSE.oneofs_by_name['right'] _GETDOCUMENTSREQUEST_ORDERCLAUSE.fields_by_name['aggregate'].message_type = _GETDOCUMENTSREQUEST_HAVINGAGGREGATE _GETDOCUMENTSREQUEST_ORDERCLAUSE.containing_type = _GETDOCUMENTSREQUEST _GETDOCUMENTSREQUEST_ORDERCLAUSE.oneofs_by_name['target'].fields.append( @@ -17913,10 +17924,26 @@ _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS.oneofs_by_name['variant'].fields.append( _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS.fields_by_name['entries']) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS.fields_by_name['entries'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS.oneofs_by_name['variant'] +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.containing_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1 +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.oneofs_by_name['value'].fields.append( + _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['count']) +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['count'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.oneofs_by_name['value'] +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.oneofs_by_name['value'].fields.append( + _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['sum']) +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['sum'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.oneofs_by_name['value'] +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.oneofs_by_name['value'].fields.append( + _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['avg']) +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['avg'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.oneofs_by_name['value'] +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.fields_by_name['entries'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.containing_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1 +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.oneofs_by_name['_skipped'].fields.append( + _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.fields_by_name['skipped']) +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.fields_by_name['skipped'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.oneofs_by_name['_skipped'] _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['documents'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_DOCUMENTS _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['counts'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTRESULTS _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['sums'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMRESULTS _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['averages'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['ranked'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.containing_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1 _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.oneofs_by_name['variant'].fields.append( _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['documents']) @@ -17930,6 +17957,9 @@ _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.oneofs_by_name['variant'].fields.append( _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['averages']) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['averages'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.oneofs_by_name['variant'] +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.oneofs_by_name['variant'].fields.append( + _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['ranked']) +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.fields_by_name['ranked'].containing_oneof = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA.oneofs_by_name['variant'] _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1.fields_by_name['data'].message_type = _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1.fields_by_name['proof'].message_type = _PROOF _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1.fields_by_name['metadata'].message_type = _RESPONSEMETADATA @@ -19922,13 +19952,6 @@ }) , - 'HavingRanking' : _reflection.GeneratedProtocolMessageType('HavingRanking', (_message.Message,), { - 'DESCRIPTOR' : _GETDOCUMENTSREQUEST_HAVINGRANKING, - '__module__' : 'platform_pb2' - # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking) - }) - , - 'HavingClause' : _reflection.GeneratedProtocolMessageType('HavingClause', (_message.Message,), { 'DESCRIPTOR' : _GETDOCUMENTSREQUEST_HAVINGCLAUSE, '__module__' : 'platform_pb2' @@ -19972,7 +19995,6 @@ _sym_db.RegisterMessage(GetDocumentsRequest.DocumentFieldValue.ValueList) _sym_db.RegisterMessage(GetDocumentsRequest.WhereClause) _sym_db.RegisterMessage(GetDocumentsRequest.HavingAggregate) -_sym_db.RegisterMessage(GetDocumentsRequest.HavingRanking) _sym_db.RegisterMessage(GetDocumentsRequest.HavingClause) _sym_db.RegisterMessage(GetDocumentsRequest.OrderClause) _sym_db.RegisterMessage(GetDocumentsRequest.GetDocumentsRequestV0) @@ -20074,6 +20096,20 @@ }) , + 'RankedEntry' : _reflection.GeneratedProtocolMessageType('RankedEntry', (_message.Message,), { + 'DESCRIPTOR' : _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY, + '__module__' : 'platform_pb2' + # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry) + }) + , + + 'RankedEntries' : _reflection.GeneratedProtocolMessageType('RankedEntries', (_message.Message,), { + 'DESCRIPTOR' : _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES, + '__module__' : 'platform_pb2' + # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries) + }) + , + 'ResultData' : _reflection.GeneratedProtocolMessageType('ResultData', (_message.Message,), { 'DESCRIPTOR' : _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA, '__module__' : 'platform_pb2' @@ -20104,6 +20140,8 @@ _sym_db.RegisterMessage(GetDocumentsResponse.GetDocumentsResponseV1.AverageEntries) _sym_db.RegisterMessage(GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregate) _sym_db.RegisterMessage(GetDocumentsResponse.GetDocumentsResponseV1.AverageResults) +_sym_db.RegisterMessage(GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry) +_sym_db.RegisterMessage(GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries) _sym_db.RegisterMessage(GetDocumentsResponse.GetDocumentsResponseV1.ResultData) GetDocumentHistoryRequest = _reflection.GeneratedProtocolMessageType('GetDocumentHistoryRequest', (_message.Message,), { @@ -22373,7 +22411,6 @@ _GETDATACONTRACTHISTORYRESPONSE_GETDATACONTRACTHISTORYRESPONSEV0_DATACONTRACTHISTORYENTRY.fields_by_name['date']._options = None _GETDOCUMENTSREQUEST_DOCUMENTFIELDVALUE.fields_by_name['int64_value']._options = None _GETDOCUMENTSREQUEST_DOCUMENTFIELDVALUE.fields_by_name['uint64_value']._options = None -_GETDOCUMENTSREQUEST_HAVINGRANKING.fields_by_name['n']._options = None _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRY.fields_by_name['count']._options = None _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTRESULTS.fields_by_name['aggregate_count']._options = None _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRY.fields_by_name['sum']._options = None @@ -22382,6 +22419,9 @@ _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRY.fields_by_name['sum']._options = None _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEAGGREGATE.fields_by_name['count']._options = None _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEAGGREGATE.fields_by_name['sum']._options = None +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['count']._options = None +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY.fields_by_name['sum']._options = None +_GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES.fields_by_name['skipped']._options = None _GETDOCUMENTHISTORYREQUEST_GETDOCUMENTHISTORYREQUESTV0.fields_by_name['start_at_ms']._options = None _GETDOCUMENTHISTORYRESPONSE_GETDOCUMENTHISTORYRESPONSEV0_DOCUMENTHISTORYENTRY.fields_by_name['date']._options = None _GETEPOCHSINFORESPONSE_GETEPOCHSINFORESPONSEV0_EPOCHINFO.fields_by_name['first_block_height']._options = None @@ -22435,8 +22475,8 @@ index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=65773, - serialized_end=74555, + serialized_start=65873, + serialized_end=74655, methods=[ _descriptor.MethodDescriptor( name='broadcastStateTransition', diff --git a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts index 75f90aec7ae..c394bd38fe9 100644 --- a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts +++ b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts @@ -2435,41 +2435,6 @@ export namespace GetDocumentsRequest { export const Function: FunctionMap; } - export class HavingRanking extends jspb.Message { - getKind(): GetDocumentsRequest.HavingRanking.KindMap[keyof GetDocumentsRequest.HavingRanking.KindMap]; - setKind(value: GetDocumentsRequest.HavingRanking.KindMap[keyof GetDocumentsRequest.HavingRanking.KindMap]): void; - - hasN(): boolean; - clearN(): void; - getN(): string; - setN(value: string): void; - - serializeBinary(): Uint8Array; - toObject(includeInstance?: boolean): HavingRanking.AsObject; - static toObject(includeInstance: boolean, msg: HavingRanking): HavingRanking.AsObject; - static extensions: {[key: number]: jspb.ExtensionFieldInfo}; - static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; - static serializeBinaryToWriter(message: HavingRanking, writer: jspb.BinaryWriter): void; - static deserializeBinary(bytes: Uint8Array): HavingRanking; - static deserializeBinaryFromReader(message: HavingRanking, reader: jspb.BinaryReader): HavingRanking; - } - - export namespace HavingRanking { - export type AsObject = { - kind: GetDocumentsRequest.HavingRanking.KindMap[keyof GetDocumentsRequest.HavingRanking.KindMap], - n: string, - } - - export interface KindMap { - MIN: 0; - MAX: 1; - TOP: 2; - BOTTOM: 3; - } - - export const Kind: KindMap; - } - export class HavingClause extends jspb.Message { hasAggregate(): boolean; clearAggregate(): void; @@ -2484,11 +2449,6 @@ export namespace GetDocumentsRequest { getValue(): GetDocumentsRequest.DocumentFieldValue | undefined; setValue(value?: GetDocumentsRequest.DocumentFieldValue): void; - hasRanking(): boolean; - clearRanking(): void; - getRanking(): GetDocumentsRequest.HavingRanking | undefined; - setRanking(value?: GetDocumentsRequest.HavingRanking): void; - getRightCase(): HavingClause.RightCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): HavingClause.AsObject; @@ -2505,7 +2465,6 @@ export namespace GetDocumentsRequest { aggregate?: GetDocumentsRequest.HavingAggregate.AsObject, operator: GetDocumentsRequest.HavingClause.OperatorMap[keyof GetDocumentsRequest.HavingClause.OperatorMap], value?: GetDocumentsRequest.DocumentFieldValue.AsObject, - ranking?: GetDocumentsRequest.HavingRanking.AsObject, } export interface OperatorMap { @@ -2527,7 +2486,6 @@ export namespace GetDocumentsRequest { export enum RightCase { RIGHT_NOT_SET = 0, VALUE = 3, - RANKING = 4, } } @@ -3244,6 +3202,82 @@ export namespace GetDocumentsResponse { } } + export class RankedEntry extends jspb.Message { + getKey(): Uint8Array | string; + getKey_asU8(): Uint8Array; + getKey_asB64(): string; + setKey(value: Uint8Array | string): void; + + hasCount(): boolean; + clearCount(): void; + getCount(): string; + setCount(value: string): void; + + hasSum(): boolean; + clearSum(): void; + getSum(): string; + setSum(value: string): void; + + hasAvg(): boolean; + clearAvg(): void; + getAvg(): number; + setAvg(value: number): void; + + getValueCase(): RankedEntry.ValueCase; + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RankedEntry.AsObject; + static toObject(includeInstance: boolean, msg: RankedEntry): RankedEntry.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RankedEntry, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RankedEntry; + static deserializeBinaryFromReader(message: RankedEntry, reader: jspb.BinaryReader): RankedEntry; + } + + export namespace RankedEntry { + export type AsObject = { + key: Uint8Array | string, + count: string, + sum: string, + avg: number, + } + + export enum ValueCase { + VALUE_NOT_SET = 0, + COUNT = 2, + SUM = 3, + AVG = 4, + } + } + + export class RankedEntries extends jspb.Message { + clearEntriesList(): void; + getEntriesList(): Array; + setEntriesList(value: Array): void; + addEntries(value?: GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, index?: number): GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry; + + hasSkipped(): boolean; + clearSkipped(): void; + getSkipped(): string; + setSkipped(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RankedEntries.AsObject; + static toObject(includeInstance: boolean, msg: RankedEntries): RankedEntries.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RankedEntries, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RankedEntries; + static deserializeBinaryFromReader(message: RankedEntries, reader: jspb.BinaryReader): RankedEntries; + } + + export namespace RankedEntries { + export type AsObject = { + entriesList: Array, + skipped: string, + } + } + export class ResultData extends jspb.Message { hasDocuments(): boolean; clearDocuments(): void; @@ -3265,6 +3299,11 @@ export namespace GetDocumentsResponse { getAverages(): GetDocumentsResponse.GetDocumentsResponseV1.AverageResults | undefined; setAverages(value?: GetDocumentsResponse.GetDocumentsResponseV1.AverageResults): void; + hasRanked(): boolean; + clearRanked(): void; + getRanked(): GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries | undefined; + setRanked(value?: GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries): void; + getVariantCase(): ResultData.VariantCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ResultData.AsObject; @@ -3282,6 +3321,7 @@ export namespace GetDocumentsResponse { counts?: GetDocumentsResponse.GetDocumentsResponseV1.CountResults.AsObject, sums?: GetDocumentsResponse.GetDocumentsResponseV1.SumResults.AsObject, averages?: GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.AsObject, + ranked?: GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.AsObject, } export enum VariantCase { @@ -3290,6 +3330,7 @@ export namespace GetDocumentsResponse { COUNTS = 2, SUMS = 3, AVERAGES = 4, + RANKED = 5, } } diff --git a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js index de7152f9dc9..10f2641e792 100644 --- a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js +++ b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js @@ -171,8 +171,6 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAgg goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.RightCase', null, { proto }); -goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking', null, { proto }); -goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause.TargetCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsRequest.VersionCase', null, { proto }); @@ -193,6 +191,9 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocum goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.VariantCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase', null, { proto }); @@ -2257,27 +2258,6 @@ if (goog.DEBUG && !COMPILED) { */ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate'; } -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); -}; -goog.inherits(proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking, jspb.Message); -if (goog.DEBUG && !COMPILED) { - /** - * @public - * @override - */ - proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking'; -} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -2698,6 +2678,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.repeatedFields_, null); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.displayName = 'proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -25454,194 +25476,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.prototype.se - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * Optional fields that are not set will be set to undefined. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * net/proto2/compiler/js/internal/generator.cc#kKeyword. - * @param {boolean=} opt_includeInstance Deprecated. whether to include the - * JSPB instance for transitional soy proto support: - * http://goto/soy-param-migration - * @return {!Object} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.toObject = function(opt_includeInstance) { - return proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Deprecated. Whether to include - * the JSPB instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} msg The msg instance to transform. - * @return {!Object} - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject = function(includeInstance, msg) { - var f, obj = { - kind: jspb.Message.getFieldWithDefault(msg, 1, 0), - n: jspb.Message.getFieldWithDefault(msg, 2, "0") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking; - return proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} */ (reader.readEnum()); - msg.setKind(value); - break; - case 2: - var value = /** @type {string} */ (reader.readUint64String()); - msg.setN(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getKind(); - if (f !== 0.0) { - writer.writeEnum( - 1, - f - ); - } - f = /** @type {string} */ (jspb.Message.getField(message, 2)); - if (f != null) { - writer.writeUint64String( - 2, - f - ); - } -}; - - -/** - * @enum {number} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind = { - MIN: 0, - MAX: 1, - TOP: 2, - BOTTOM: 3 -}; - -/** - * optional Kind kind = 1; - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.getKind = function() { - return /** @type {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); -}; - - -/** - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.Kind} value - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.setKind = function(value) { - return jspb.Message.setProto3EnumField(this, 1, value); -}; - - -/** - * optional uint64 n = 2; - * @return {string} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.getN = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); -}; - - -/** - * @param {string} value - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.setN = function(value) { - return jspb.Message.setField(this, 2, value); -}; - - -/** - * Clears the field making it undefined. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.clearN = function() { - return jspb.Message.setField(this, 2, undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.hasN = function() { - return jspb.Message.getField(this, 2) != null; -}; - - - /** * Oneof group definitions for this message. Each group defines the field * numbers belonging to that group. When of these fields' value is set, all @@ -25650,15 +25484,14 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.prototype.hasN * @private {!Array>} * @const */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.oneofGroups_ = [[3,4]]; +proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.oneofGroups_ = [[3]]; /** * @enum {number} */ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.RightCase = { RIGHT_NOT_SET: 0, - VALUE: 3, - RANKING: 4 + VALUE: 3 }; /** @@ -25701,8 +25534,7 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.toObject = func var f, obj = { aggregate: (f = msg.getAggregate()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.toObject(includeInstance, f), operator: jspb.Message.getFieldWithDefault(msg, 2, 0), - value: (f = msg.getValue()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.toObject(includeInstance, f), - ranking: (f = msg.getRanking()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.toObject(includeInstance, f) + value: (f = msg.getValue()) && proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.toObject(includeInstance, f) }; if (includeInstance) { @@ -25753,11 +25585,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.deserializeBina reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.deserializeBinaryFromReader); msg.setValue(value); break; - case 4: - var value = new proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking; - reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.deserializeBinaryFromReader); - msg.setRanking(value); - break; default: reader.skipField(); break; @@ -25810,14 +25637,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.serializeBinary proto.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.serializeBinaryToWriter ); } - f = message.getRanking(); - if (f != null) { - writer.writeMessage( - 4, - f, - proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking.serializeBinaryToWriter - ); - } }; @@ -25930,43 +25749,6 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.hasVa }; -/** - * optional HavingRanking ranking = 4; - * @return {?proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.getRanking = function() { - return /** @type{?proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking} */ ( - jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking, 4)); -}; - - -/** - * @param {?proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingRanking|undefined} value - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause} returns this -*/ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.setRanking = function(value) { - return jspb.Message.setOneofWrapperField(this, 4, proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.oneofGroups_[0], value); -}; - - -/** - * Clears the message field making it undefined. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause} returns this - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.clearRanking = function() { - return this.setRanking(undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.prototype.hasRanking = function() { - return jspb.Message.getField(this, 4) != null; -}; - - /** * Oneof group definitions for this message. Each group defines the field @@ -30824,24 +30606,23 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Aver * @private {!Array>} * @const */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_ = [[1,2,3,4]]; +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_ = [[2,3,4]]; /** * @enum {number} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase = { - VARIANT_NOT_SET: 0, - DOCUMENTS: 1, - COUNTS: 2, - SUMS: 3, - AVERAGES: 4 +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase = { + VALUE_NOT_SET: 0, + COUNT: 2, + SUM: 3, + AVG: 4 }; /** - * @return {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} + * @return {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.getVariantCase = function() { - return /** @type {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} */(jspb.Message.computeOneofCase(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_[0])); +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getValueCase = function() { + return /** @type {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.ValueCase} */(jspb.Message.computeOneofCase(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0])); }; @@ -30859,8 +30640,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) { * http://goto/soy-param-migration * @return {!Object} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.toObject = function(opt_includeInstance) { - return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject(opt_includeInstance, this); +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject(opt_includeInstance, this); }; @@ -30869,16 +30650,16 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu * @param {boolean|undefined} includeInstance Deprecated. Whether to include * the JSPB instance for transitional soy proto support: * http://goto/soy-param-migration - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The msg instance to transform. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} msg The msg instance to transform. * @return {!Object} * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject = function(includeInstance, msg) { +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject = function(includeInstance, msg) { var f, obj = { - documents: (f = msg.getDocuments()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.toObject(includeInstance, f), - counts: (f = msg.getCounts()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.toObject(includeInstance, f), - sums: (f = msg.getSums()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.toObject(includeInstance, f), - averages: (f = msg.getAverages()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.toObject(includeInstance, f) + key: msg.getKey_asB64(), + count: jspb.Message.getFieldWithDefault(msg, 2, "0"), + sum: jspb.Message.getFieldWithDefault(msg, 3, "0"), + avg: jspb.Message.getFloatingPointFieldWithDefault(msg, 4, 0.0) }; if (includeInstance) { @@ -30892,19 +30673,555 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu /** * Deserializes binary data (in protobuf wire format). * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} */ -proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinary = function(bytes) { +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinary = function(bytes) { var reader = new jspb.BinaryReader(bytes); - var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData; - return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinaryFromReader(msg, reader); + var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry; + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinaryFromReader(msg, reader); }; /** * Deserializes binary data (in protobuf wire format) from the * given reader into the given message object. - * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The message object to deserialize into. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setKey(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setCount(value); + break; + case 3: + var value = /** @type {string} */ (reader.readSint64String()); + msg.setSum(value); + break; + case 4: + var value = /** @type {number} */ (reader.readDouble()); + msg.setAvg(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getKey_asU8(); + if (f.length > 0) { + writer.writeBytes( + 1, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeUint64String( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeSint64String( + 3, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeDouble( + 4, + f + ); + } +}; + + +/** + * optional bytes key = 1; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getKey = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * optional bytes key = 1; + * This is a type-conversion wrapper around `getKey()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getKey_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getKey())); +}; + + +/** + * optional bytes key = 1; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getKey()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getKey_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getKey())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setKey = function(value) { + return jspb.Message.setProto3BytesField(this, 1, value); +}; + + +/** + * optional uint64 count = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getCount = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setCount = function(value) { + return jspb.Message.setOneofField(this, 2, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.clearCount = function() { + return jspb.Message.setOneofField(this, 2, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.hasCount = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional sint64 sum = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getSum = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setSum = function(value) { + return jspb.Message.setOneofField(this, 3, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.clearSum = function() { + return jspb.Message.setOneofField(this, 3, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.hasSum = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional double avg = 4; + * @return {number} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.getAvg = function() { + return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 4, 0.0)); +}; + + +/** + * @param {number} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.setAvg = function(value) { + return jspb.Message.setOneofField(this, 4, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.clearAvg = function() { + return jspb.Message.setOneofField(this, 4, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.prototype.hasAvg = function() { + return jspb.Message.getField(this, 4) != null; +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject = function(includeInstance, msg) { + var f, obj = { + entriesList: jspb.Message.toObjectList(msg.getEntriesList(), + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.toObject, includeInstance), + skipped: jspb.Message.getFieldWithDefault(msg, 2, "0") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries; + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.deserializeBinaryFromReader); + msg.addEntries(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setSkipped(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getEntriesList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 1, + f, + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry.serializeBinaryToWriter + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeUint64String( + 2, + f + ); + } +}; + + +/** + * repeated RankedEntry entries = 1; + * @return {!Array} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.getEntriesList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this +*/ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.setEntriesList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 1, value); +}; + + +/** + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry=} opt_value + * @param {number=} opt_index + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.addEntries = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.clearEntriesList = function() { + return this.setEntriesList([]); +}; + + +/** + * optional uint64 skipped = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.getSkipped = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.setSkipped = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.clearSkipped = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.prototype.hasSkipped = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_ = [[1,2,3,4,5]]; + +/** + * @enum {number} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase = { + VARIANT_NOT_SET: 0, + DOCUMENTS: 1, + COUNTS: 2, + SUMS: 3, + AVERAGES: 4, + RANKED: 5 +}; + +/** + * @return {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.getVariantCase = function() { + return /** @type {proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.VariantCase} */(jspb.Message.computeOneofCase(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_[0])); +}; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.toObject = function(includeInstance, msg) { + var f, obj = { + documents: (f = msg.getDocuments()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Documents.toObject(includeInstance, f), + counts: (f = msg.getCounts()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResults.toObject(includeInstance, f), + sums: (f = msg.getSums()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResults.toObject(includeInstance, f), + averages: (f = msg.getAverages()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.toObject(includeInstance, f), + ranked: (f = msg.getRanked()) && proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData; + return proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} msg The message object to deserialize into. * @param {!jspb.BinaryReader} reader The BinaryReader to use. * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} */ @@ -30935,6 +31252,11 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.deserializeBinaryFromReader); msg.setAverages(value); break; + case 5: + var value = new proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.deserializeBinaryFromReader); + msg.setRanked(value); + break; default: reader.skipField(); break; @@ -30996,6 +31318,14 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResults.serializeBinaryToWriter ); } + f = message.getRanked(); + if (f != null) { + writer.writeMessage( + 5, + f, + proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries.serializeBinaryToWriter + ); + } }; @@ -31147,6 +31477,43 @@ proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.Resu }; +/** + * optional RankedEntries ranked = 5; + * @return {?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.getRanked = function() { + return /** @type{?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries} */ ( + jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries, 5)); +}; + + +/** + * @param {?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntries|undefined} value + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} returns this +*/ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.setRanked = function(value) { + return jspb.Message.setOneofWrapperField(this, 5, proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} returns this + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.clearRanked = function() { + return this.setRanked(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData.prototype.hasRanked = function() { + return jspb.Message.getField(this, 5) != null; +}; + + /** * optional ResultData data = 1; * @return {?proto.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultData} diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index e584130d231..c93f36b0a19 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -661,9 +661,13 @@ message GetDocumentsRequest { } // Per-group aggregate operand for the left side of a - // `HavingClause`. Only the per-group aggregates live here: - // `MIN` / `MAX` / `TOP` / `BOTTOM` are **cross-group** ranking - // primitives and appear on the right side via `HavingRanking`. + // `HavingClause`, and the aggregate-function target of an + // `OrderClause`. Only the per-group aggregates live here — + // `HAVING` is a boolean predicate over one group's own aggregate, + // and nothing on this message reaches across groups. Cross-group + // ranking ("which groups score highest?") is expressed with SQL's + // ordering surface instead: `ORDER BY + // DESC LIMIT n [OFFSET m]`. See `GetDocumentsRequestV1.order_by`. // // **Field semantics by function**: // - `COUNT`: empty `field` means `COUNT(*)` (group cardinality); @@ -682,58 +686,42 @@ message GetDocumentsRequest { string field = 2; } - // Cross-group ranking primitive on the right side of a - // `HavingClause`. The ranking is computed over the set of - // group-aggregate results (one per `GROUP BY` row), so - // `HAVING COUNT(*) EQ MAX` selects groups whose count equals - // the maximum count across all groups, and - // `HAVING COUNT(*) IN TOP(5)` selects groups whose count is - // among the five largest. Concise way to express top-N / - // bottom-N selection without window functions or - // `ORDER BY` + `LIMIT`. - // - // **Operator compatibility**: - // - Scalar operators (`=`, `!=`, `<`, `<=`, `>`, `>=`) work - // with `MIN` / `MAX`. `TOP` / `BOTTOM` with scalar operators - // only make sense when `n=1` (the single largest / smallest); - // evaluation rejects other combinations as ambiguous. - // - `IN` works with `TOP(n)` / `BOTTOM(n)` for set membership. - // - `BETWEEN*` doesn't compose meaningfully with rankings and - // is rejected at evaluation time. - message HavingRanking { - enum Kind { - MIN = 0; - MAX = 1; - TOP = 2; - BOTTOM = 3; - } - Kind kind = 1; - // N-th rank for `TOP` / `BOTTOM` (1-indexed: `n=1` is the - // single largest / smallest). Required for those two kinds; - // must be unset for `MIN` / `MAX`. The wire allows setting - // it on `MIN` / `MAX` for forward compatibility, but - // evaluation rejects it as a malformed ranking. - optional uint64 n = 2 [jstype = JS_STRING]; - } - - // Single `HAVING ` clause. Multiple + // Single `HAVING ` clause. Multiple // entries in `GetDocumentsRequestV1.having` combine with // implicit AND — same semantics as multiple `where_clauses` // entries. `HAVING COUNT(*) > 5 AND SUM(amount) > 100` is two // `HavingClause` rows, not a tree. // + // **`HAVING` is a boolean per-group predicate and nothing else.** + // Its right operand is always a literal `DocumentFieldValue`; it + // never names another group or the set of groups. Cross-group + // ranking — "the 5 highest-scoring groups" — is `ORDER BY DESC LIMIT 5`, exactly as in SQL, and is + // served by the ranked executor (protocol v14+). An earlier draft + // of this surface carried the ranking on the right of a `HAVING` + // (`HAVING AVG(grade) IN TOP(5)`); that spelling was removed + // before release rather than deprecated, because it invented + // non-SQL grammar for something SQL already expresses. + // + // **`HAVING` cannot yet combine with an aggregate `ORDER BY`.** + // The ranked executor reads a pre-sorted per-axis secondary and + // has no way to drop groups from the middle of that walk, so a + // request carrying both a non-empty `having` and a ranking + // `order_by` is rejected with `Unsupported` rather than served + // with one of the two silently ignored. + // // The operator set mirrors `WhereOperator` minus `STARTS_WITH` // (prefix matching has no natural meaning against a scalar // aggregate result, even a string-typed one). `BETWEEN*` and // `IN` operand semantics match `WhereOperator`: `BETWEEN*` // expects a 2-element `DocumentFieldValue.list` carrying // `[lower, upper]`, and `IN` expects a `list` of candidate - // values (or a ranking set via `right.ranking`). + // values. // - // The `right` oneof carries either a concrete - // `DocumentFieldValue` (literal comparison target) or a - // `HavingRanking` (cross-group reference). Exactly one is set; - // the wire rejects an unset `right`. + // The `right` oneof exists (rather than a bare + // `DocumentFieldValue` field) so the "unset right operand" case + // stays distinguishable from "the literal null value"; the wire + // rejects an unset `right`. message HavingClause { enum Operator { EQUAL = 0; @@ -752,33 +740,65 @@ message GetDocumentsRequest { Operator operator = 2; oneof right { DocumentFieldValue value = 3; - HavingRanking ranking = 4; } } - // Single `ORDER BY field ` clause. Multi-field + // Single `ORDER BY ` clause. Multi-field // ordering is expressed by repeating this message at the // request level (`repeated OrderClause order_by = 4`), matching // SQL's `ORDER BY a ASC, b DESC` shape. - // Single ORDER BY entry. Multi-entry ordering is expressed by - // repeating this message at the request level. // // The `target` oneof carries either a plain field name // (`ORDER BY field`) or an aggregate function applied to a - // field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`) — the - // latter sorts per-group result rows produced by `GROUP BY`, - // useful with `LIMIT` for top-N / bottom-N selection at the - // routing layer (overlapping `HavingRanking::Top` / `Bottom` - // but more general because the ranking field can be any - // aggregate, not just count). + // field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`). + // + // **Two distinct roles ride the `field` target.** + // + // 1. *Row ordering* — `select = DOCUMENTS`. `field` names a + // document property and the matched rows come back in that + // order. This is v0's behaviour, unchanged. + // + // 2. *Aggregate ordering* — the **ranked** surface (protocol + // v14+). With a `GROUP BY` and a single aggregate `select`, + // exactly one `order_by` clause naming that select's + // aggregate orders the *groups* by their aggregate value and + // routes the request to the ranked executor: // - // **Aggregate target currently rejected** with - // `Unsupported("ORDER BY on aggregate is not yet implemented")`. - // The wire surface is shipped now so callers can encode the - // shape ahead of server support landing. + // ```text + // SELECT AVG(grade) GROUP BY restaurantId + // ORDER BY grade DESC LIMIT 3 -- 3 best restaurants + // SELECT COUNT(*) GROUP BY restaurantId + // ORDER BY $count DESC LIMIT 10 OFFSET 10 -- busiest, page 2 + // ``` + // + // `SUM(f)` / `AVG(f)` are named by `f` — the same property the + // projection aggregates, which is how `ORDER BY avg(grade)` + // reads once `SELECT` has already fixed the function. + // `COUNT(*)` aggregates no property, so it is named by the + // reserved sentinel **`$count`**. The `$` prefix is what keeps + // the sentinel from colliding with a real document property: + // document properties cannot start with `$` (that namespace is + // the system fields' `$id` / `$ownerId` / …), so `$count` can + // never be mistaken for a column. `DESC` is the "top n" + // reading, `ASC` the "bottom n" reading. + // + // An `order_by` naming anything other than the selected + // aggregate — a second clause, the `GROUP BY` property, an + // unrelated field — is rejected rather than normalized: it + // asks for an ordering the ranked secondary cannot produce. + // + // **Aggregate target still rejected** with + // `Unsupported("ORDER BY on aggregate keys is not yet + // implemented")`. It is the *explicit* spelling of role 2 + // (`ORDER BY AVG(grade)` rather than `ORDER BY grade` under a + // `SELECT AVG(grade)`) and is wire-stable so it can start being + // evaluated without another version bump; today the field-target + // spelling above is the one the ranked executor reads. message OrderClause { oneof target { - // Plain field name. Today's evaluated form. + // Plain field name. Today's evaluated form — a document + // property for row ordering, or the selected aggregate's + // property (`$count` for `COUNT(*)`) for aggregate ordering. string field = 1; // Aggregate function applied to a field, sorted by the // per-group result. `function = DOCUMENTS` is invalid @@ -823,12 +843,18 @@ message GetDocumentsRequest { // other shapes return `Unsupported` (see supported-shape table // below). // - // `having` is wire-reserved for a future server capability. Any - // non-empty `having` list currently returns - // `Unsupported("HAVING clause is not yet implemented")` - // regardless of `select` / `group_by`. The wire shape is - // `repeated WhereClause` so when execution lands the surface is - // already typed end-to-end and callers don't need to re-encode. + // **Ranked mode** is served from protocol v14 and is selected by + // `group_by` + a single `order_by` naming the selected aggregate — + // SQL's own top-n spelling, `ORDER BY DESC LIMIT n OFFSET m`. + // It returns `ResultData.ranked`. See `order_by` and the + // supported-shape table below. + // + // `having` is a boolean per-group predicate and is **still** + // `Unsupported` at every protocol version, ranked mode or not + // (`"HAVING clause is not yet implemented"`). It carries no ranking + // spelling: an earlier draft put cross-group ranking on the right of + // a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that grammar was + // removed before release in favour of `ORDER BY` + `LIMIT`. // // **Supported shapes** (everything else rejects with a typed // `QuerySyntaxError::Unsupported` so callers can detect un-wired @@ -853,8 +879,13 @@ message GetDocumentsRequest { // `select=COUNT, group_by=[a, b]`: // - a is the In field AND b is the range field, in that order → existing compound distinct shape; entries carry both `in_key` (= a's value) and `key` (= b's value). // + // `select=, group_by=[p], order_by=[]` (protocol v14+) — **ranked mode**: + // - exactly one `group_by` property, exactly one `order_by` clause naming the select's aggregate (`f` for `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)`), a `limit` in `1 ..= 100`, an optional `offset`, and no `where` / `having` / `start_at`, on an index declaring the matching `rankedCountable` / `rankedSummable` / `rankedAverageable` axis → ranked executor, answered in `ResultData.ranked`. + // - `DESC` is the "top n" reading (walk the axis from the largest aggregate down), `ASC` the "bottom n" reading. Worked example: `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 1 OFFSET 4` is the 5th-best restaurant. + // // **Rejected shapes** (return `Unsupported`): - // - any non-empty `having` (always — pending future server capability). + // - any non-empty `having`, at every protocol version. + // - at v14+: a ranked-shaped request carrying a `where` clause, a `start_at` / `start_after` cursor, more than one `order_by`, or an `order_by` naming anything but the selected aggregate. // - `select=DOCUMENTS` with non-empty `group_by`. // - `select=COUNT` with `group_by` on a field that is not constrained by an `In` or range where clause. // - `select=COUNT` with `group_by.len() > 2`. @@ -927,11 +958,11 @@ message GetDocumentsRequest { AVG = 3; // Per-group MIN / MAX — `SELECT MIN(field) GROUP BY // category` returns the smallest `field` value in each - // category. Semantically distinct from - // `HavingRanking::Min` / `Max` (which are cross-group - // meta-aggregates over group results). MIN/MAX here - // operate over the row values within each group, the - // same way `SUM` and `AVG` do. + // category. These operate over the row values *within* + // each group, the same way `SUM` and `AVG` do; they are + // not a cross-group ranking. To pick out the extreme + // *group*, order by the aggregate instead: + // `ORDER BY ASC|DESC LIMIT 1` (see `order_by`). MIN = 4; MAX = 5; } @@ -1044,33 +1075,54 @@ message GetDocumentsRequest { // message-level docstring for the supported-shape table. repeated string group_by = 10; - // SQL `HAVING` clauses — aggregate filters that apply to the - // grouped rows produced by `select=COUNT, group_by=[…]`. The - // wire shape is `HavingClause`, not `WhereClause`, because - // HAVING evaluates against per-group aggregates - // (`COUNT`/`SUM`/`AVG`/`MIN`/`MAX`/`TOP`/`BOTTOM`) rather than - // row field values. Multiple entries combine with implicit - // AND. See `HavingClause` / `HavingAggregate` for the - // operator and aggregate-function catalogs. + // SQL `HAVING` clauses — **boolean** aggregate filters that + // apply to the grouped rows produced by `select=, + // group_by=[…]`. The wire shape is `HavingClause`, not + // `WhereClause`, because HAVING evaluates against per-group + // aggregates (`COUNT` / `SUM` / `AVG`) rather than row field + // values. Multiple entries combine with implicit AND. See + // `HavingClause` / `HavingAggregate` for the operator and + // aggregate-function catalogs. // - // **Always rejected when non-empty** today with - // `Unsupported("HAVING clause is not yet implemented")`. The - // wire shape is shipped now so the future server capability - // can land without another version bump — and so callers can - // construct full `HAVING COUNT(*) > 5 AND SUM(amount) > 100` - // requests in their builders even before the server evaluates - // them. + // **Every non-empty `having` is rejected**, at every protocol + // version, with `Unsupported("HAVING clause is not yet + // implemented")`. The wire shape ships ahead of evaluation so + // callers can construct full `HAVING COUNT(*) > 5 AND + // SUM(amount) > 100` requests in their builders, and so the + // capability can land without another version bump. + // + // **`having` does not express ranking.** "The n highest-scoring + // groups" is `ORDER BY DESC LIMIT n` + // (see `order_by`), which *is* served, from protocol v14. An + // earlier draft of this surface spelled it + // `HAVING AVG(grade) IN TOP(5)`; that grammar was removed + // before release rather than deprecated. repeated HavingClause having = 11; // Row-based pagination offset, on top of the cursor-based // `start_after` / `start_at` pagination. `OFFSET N` skips the - // first `N` matching rows before applying `limit`. Currently - // **always rejected when non-`None`** with - // `Unsupported("OFFSET pagination is not yet implemented")` - // — the wire surface is shipped now so callers can encode it - // ahead of server support landing without another version - // bump. Cursor pagination via `start_after` / `start_at` - // remains the supported way to page through results. + // first `N` result rows before applying `limit`. + // + // **Consumed in ranked mode** (protocol v14+): on a request that + // routes to the ranked executor (`group_by` + a single `order_by` + // naming the selected aggregate), `offset` skips that many ranks + // before the returned page, so `ORDER BY avg(grade) DESC LIMIT 1 + // OFFSET 4` is the 5th-best group. The skip is **count-attested**, + // not walked: grovedb proves it from the counted subtree + // commitments, so the proof stays `O(log n + k)` at any offset and + // the response echoes the attested number in + // `RankedEntries.skipped`. There is deliberately no ceiling — an + // offset of 4 and an offset of four billion cost the same, so + // there is no denial-of-service lever a cap would close. An offset + // past the end of the ranking is a provable answer rather than an + // error: `entries` comes back empty and `skipped` is the ranking's + // whole population. + // + // **Rejected everywhere else** with + // `Unsupported("OFFSET pagination is not yet implemented")` — + // including on every path at protocol v13 and earlier, which has + // no ranked executor. Cursor pagination via `start_after` / + // `start_at` remains the supported way to page through documents. optional uint32 offset = 12; } @@ -1258,9 +1310,115 @@ message GetDocumentsResponse { } } + // One group in a ranked (`GROUP BY … ORDER BY LIMIT n`) + // result: the group's index key plus the aggregate it was ranked + // by. + // + // `key` is the raw index-key bytes of the GROUP BY property's + // value — the same bytes that name the group's value tree under + // the index (for a `string` property, its UTF-8 bytes). Clients + // that want the typed value decode it with the document type's + // key deserialization; the wire carries bytes so prover and + // verifier agree without a schema round-trip. + // + // Exactly one `value` variant is set, determined by the + // request's SELECT function: + // * `count` — `SELECT COUNT(*)`, ranked on the index's + // `rankedCountable` axis. + // * `sum` — `SELECT SUM(field)`, `rankedSummable` axis. + // Signed for the same reason `SumEntry.sum` is. + // * `avg` — `SELECT AVG(field)`, + // `rankedAverageable` axis. + message RankedEntry { + bytes key = 1; + oneof value { + // `jstype = JS_STRING` so JS/Web clients receive a string + // and don't round counts > 2^53−1 to the nearest + // representable Number — same choice as `CountEntry.count`. + uint64 count = 2 [jstype = JS_STRING]; + // `jstype = JS_STRING` for the same precision reason as + // `SumEntry.sum`. + sint64 sum = 3 [jstype = JS_STRING]; + // The group's average, as a **`double` approximation** of the + // exact value the Avg axis is ordered by. + // + // What grovedb actually commits to and sorts by is an `i128` + // fixed-point integer: `floor(sum * SCALE / count)` with + // euclidean (toward −∞) division, where SCALE is grovedb's + // `AVG_FIXED_POINT_SCALE` (currently 10^19). This field is + // that integer divided by SCALE in `f64`, i.e. + // `fixed_point as f64 / SCALE as f64`. + // + // A `double` is honest here because `RankedEntry` is only ever + // populated on the **no-proof ("quick answer") path**, where + // the client has already chosen to trust the server's reply. + // A proof-verifying client never reads this field: it + // reconstructs each entry from the grovedb proof itself, where + // the exact fixed-point `i128` lives, so nothing about proof + // verification depends on this number's precision. + // + // Precision bound: `f64` carries ~15–16 significant decimal + // digits, so two groups whose exact fixed-point averages differ + // only beyond that can compare equal here. Do not use this + // value for equality checks, tie-breaking, or any + // reconstruction of the committed integer — request the proof + // and read the fixed point from it instead. Entry *order* is + // still exact: the server ranks on the i128 before converting. + // + // SCALE is a grovedb constant, not a wire constant: it moved + // from 10^15 to 10^19 before release. Clients that need it + // (e.g. to go back to fixed point on the proof path) should + // read it from the SDK's re-export (`RANKED_AVG_SCALE`) rather + // than hardcoding the literal. + double avg = 4; + } + } + + // Ranked result entries. **Entry order IS the ranking order** — + // best-first for `ORDER BY DESC`, worst-first for `ASC`. + // Clients must not re-sort; ties (equal aggregates) come back in + // group-key order in the direction of the walk, which is + // descending group-key order for `DESC`. + // + // Fewer than `limit` entries is normal — the index simply has + // fewer groups than requested — and is not an error. + message RankedEntries { + repeated RankedEntry entries = 1; + + // How many groups were skipped before the first entry — the + // page's **starting rank base**. Entry `i` of `entries` is the + // group at rank `skipped + i` (0-based). + // + // `0` for an `OFFSET 0` (or offset-less) query, which is what + // makes this field additive: a caller that never paginates sees + // the value it would have assumed. For `OFFSET m` it is + // normally `m`, and it is what turns a page back into a + // *ranking* — without it, a caller who asked for + // `ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` receives one + // entry with no way to tell that it really is the 5th-best + // group rather than the best. + // + // **When a requested offset exceeds the population**, `entries` + // is empty and `skipped` is the ranking's attested *total* + // population — a positive, useful answer ("there are only 12 + // groups") rather than a bare empty list. + // + // On the proved path the number is grovedb's cryptographically + // attested count, re-derived by the verifier from the counted + // subtree commitments in the proof bytes rather than trusted + // from this field; a proving client should use the verified + // value. On the unproven read there is nothing to attest and + // grovedb's read API does not report a short walk, so the server + // echoes the requested offset. The two therefore disagree in + // exactly one case — an offset past the end, where the unproven + // read reports the request and the proved one reports the truth. + // Callers who need the population must prove. + optional uint64 skipped = 2 [jstype = JS_STRING]; + } + // Non-proof result wrapper. The outer `oneof result` switches // between this and `proof`; this inner oneof switches between - // the four non-proof shapes the v1 surface can return. + // the non-proof shapes the v1 surface can return. message ResultData { oneof variant { Documents documents = 1; @@ -1280,6 +1438,18 @@ message GetDocumentsResponse { // `book/src/drive/average-index-examples.md` for the design // and the grades-contract worked example. AverageResults averages = 4; + // Ranked-aggregate result. Routed when the request pairs a + // single-property `group_by` with a single `order_by` clause + // naming the single aggregate `select` (the `$count` sentinel + // for `COUNT(*)`) and a `limit` — SQL's `ORDER BY + // DESC LIMIT n [OFFSET m]`. Answered from the per-axis + // secondary of an indexed tree, so the index must declare the + // matching `rankedCountable` / `rankedSummable` / + // `rankedAverageable` keyword (meta-schema v3, protocol + // version 14+). Entry order is the ranking order, and + // `skipped` carries the page's starting rank — see + // `RankedEntries`. + RankedEntries ranked = 5; } } diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 3766527694a..7a1c3bf85f7 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -71,7 +71,7 @@ strum = { version = "0.26", features = ["derive"] } json-schema-compatibility-validator = { path = '../rs-json-schema-compatibility-validator', optional = true } once_cell = "1.19.0" tracing = { version = "0.1.41" } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", optional = true } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", optional = true } [dev-dependencies] tokio = { version = "1.40", features = ["full"] } diff --git a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json new file mode 100644 index 00000000000..b52d3ffc160 --- /dev/null +++ b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json @@ -0,0 +1,754 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", + "$comment": "EDITABLE UNTIL THE RELEASE CARRYING PROTOCOL V14 SHIPS — FROZEN AFTER. This v3 document meta-schema activates with protocol v14 (CONTRACT_VERSIONS_V6). It is v2 plus the ranked index keywords (rankedCountable, rankedSummable, rankedAverageable), and admits every v14+ contract written to disk. v2 stays in place for protocol v13, where those keys still fail an index entry's `additionalProperties: false`. Once the release carrying protocol v14 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v4+). The $id above deliberately still names the v1 path: v1, v2 and v3 all share that identity, and it is the exact string `enrich_with_base_schema` injects as every PV12+ document schema's `$schema`, so bumping it here would be a wire-visible change rather than a documentation fix.", + "type": "object", + "$defs": { + "documentProperties": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9-_]{1,64}$": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/documentSchema" + } + ], + "unevaluatedProperties": false + } + }, + "propertyNames": { + "pattern": "^[a-zA-Z0-9-_]{1,64}$" + }, + "minProperties": 1, + "maxProperties": 100 + }, + "documentSchemaArray": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/documentSchema" + } + ], + "unevaluatedProperties": false + } + }, + "documentSchema": { + "type": "object", + "properties": { + "$id": { + "type": "string", + "pattern": "^#", + "minLength": 1 + }, + "$ref": { + "type": "string", + "pattern": "^#", + "minLength": 1 + }, + "$comment": { + "$ref": "https://json-schema.org/draft/2020-12/meta/core#/properties/$comment" + }, + "description": { + "$ref": "https://json-schema.org/draft/2020-12/meta/meta-data#/properties/description" + }, + "examples": { + "$ref": "https://json-schema.org/draft/2020-12/meta/meta-data#/properties/examples" + }, + "multipleOf": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/multipleOf" + }, + "maximum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maxLength" + }, + "minLength": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minLength" + }, + "pattern": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/pattern" + }, + "maxItems": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maxItems" + }, + "minItems": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minItems" + }, + "uniqueItems": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/uniqueItems" + }, + "contains": { + "$ref": "https://json-schema.org/draft/2020-12/meta/applicator#/properties/contains" + }, + "maxProperties": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maxProperties" + }, + "minProperties": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minProperties" + }, + "required": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/required" + }, + "additionalProperties": { + "type": "boolean", + "const": false + }, + "properties": { + "$ref": "#/$defs/documentProperties" + }, + "dependentRequired": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/$defs/stringArray" + } + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/type" + }, + "format": { + "$ref": "https://json-schema.org/draft/2020-12/meta/format-annotation#/properties/format" + }, + "contentMediaType": { + "$ref": "https://json-schema.org/draft/2020-12/meta/content#/properties/contentMediaType" + }, + "byteArray": { + "type": "boolean", + "const": true + }, + "position": { + "type": "integer", + "minimum": 0 + } + }, + "dependentSchemas": { + "byteArray": { + "description": "should be used only with array type", + "properties": { + "type": { + "type": "string", + "const": "array" + } + } + }, + "contentMediaType": { + "if": { + "properties": { + "contentMediaType": { + "const": "application/x.dash.dpp.identifier" + } + } + }, + "then": { + "properties": { + "byteArray": { + "const": true + }, + "minItems": { + "const": 32 + }, + "maxItems": { + "const": 32 + } + }, + "required": [ + "byteArray", + "minItems", + "maxItems" + ] + } + }, + "pattern": { + "description": "prevent slow pattern matching of large strings", + "properties": { + "maxLength": { + "type": "integer", + "minimum": 0, + "maximum": 50000 + } + }, + "required": [ + "maxLength" + ] + }, + "format": { + "description": "prevent slow format validation of large strings", + "properties": { + "maxLength": { + "type": "integer", + "minimum": 0, + "maximum": 50000 + } + }, + "required": [ + "maxLength" + ] + } + }, + "allOf": [ + { + "$comment": "require index for object properties", + "if": { + "properties": { + "type": { + "const": "object" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "properties": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "position": true + }, + "required": [ + "position" + ] + } + } + } + } + }, + { + "$comment": "allow only byte arrays", + "if": { + "properties": { + "type": { + "const": "array" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "byteArray": true + }, + "required": [ + "byteArray" + ] + } + }, + { + "$comment": "all object properties must be defined", + "if": { + "properties": { + "type": { + "const": "object" + } + }, + "not": { + "properties": { + "$ref": true + }, + "required": [ + "$ref" + ] + } + }, + "then": { + "properties": { + "properties": { + "$ref": "#/$defs/documentProperties" + }, + "additionalProperties": { + "$ref": "#/$defs/documentSchema/properties/additionalProperties" + } + }, + "required": [ + "properties", + "additionalProperties" + ] + } + } + ] + }, + "documentActionTokenCost": { + "type": "object", + "properties": { + "contractId": { + "type": "array", + "contentMediaType": "application/x.dash.dpp.identifier", + "byteArray": true, + "minItems": 32, + "maxItems": 32 + }, + "tokenPosition": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "amount": { + "type": "integer", + "minimum": 1, + "maximum": 281474976710655 + }, + "effect": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "0 - TransferTokenToContractOwner (default), 1 - Burn" + }, + "gasFeesPaidBy": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "0 - DocumentOwner (default), 1 - ContractOwner, 2 - PreferContractOwner" + } + }, + "required": [ + "tokenPosition", + "amount" + ], + "additionalProperties": false + } + }, + "properties": { + "type": { + "type": "string", + "const": "object" + }, + "$schema": { + "type": "string", + "const": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json" + }, + "$defs": { + "$ref": "#/$defs/documentProperties" + }, + "indices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "properties": { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "maxLength": 256 + }, + "additionalProperties": { + "type": "string", + "enum": [ + "asc" + ] + }, + "minProperties": 1, + "maxProperties": 1 + }, + "minItems": 1, + "maxItems": 10 + }, + "unique": { + "type": "boolean" + }, + "nullSearchable": { + "type": "boolean" + }, + "contested": { + "type": "object", + "properties": { + "fieldMatches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "regexPattern": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "additionalProperties": false, + "required": [ + "field", + "regexPattern" + ] + }, + "minItems": 1 + }, + "resolution": { + "type": "integer", + "enum": [ + 0 + ], + "description": "Resolution. 0 - Masternode Vote" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resolution" + ], + "additionalProperties": false + }, + "countable": { + "oneOf": [ + { + "type": "boolean", + "description": "Legacy form. true == \"countable\", false == \"notCountable\". Kept for back-compat with contracts written before the enum form was introduced." + }, + { + "type": "string", + "enum": ["notCountable", "countable", "countableAllowingOffset"], + "description": "\"countable\" — index uses a CountTree (O(1) totals). \"countableAllowingOffset\" — index uses a ProvableCountTree (totals + future O(log n) range / offset queries). \"notCountable\" — plain NormalTree (no count fast path)." + } + ], + "description": "Whether and how the index supports count fast paths. Adds extra storage cost for non-default values." + }, + "rangeCountable": { + "type": "boolean", + "description": "When true, the property-name level becomes a ProvableCountTree and value trees become CountTrees so range-count queries on the indexed property are O(log n). Requires `countable` to be \"countable\" or \"countableAllowingOffset\"." + }, + "summable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Name of an integer document property whose values are aggregated into a sum at the index. When set, the index's value trees become SumTrees and each per-document index reference is a ReferenceWithSumItem contributing the named property's value to ancestor sum-bearing trees. The property must exist on the document type, be in `required`, and have an integer type." + }, + "rangeSummable": { + "type": "boolean", + "description": "When true, the property-name level becomes a ProvableSumTree (or ProvableCountProvableSumTree when paired with `rangeCountable: true`) so range-sum queries on the indexed property are O(log n) via the `AggregateSumOnRange` proof primitive. Requires `summable` to be set." + }, + "averageable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Syntactic sugar: `averageable: \"\"` is shorthand for `countable: \"countable\"` + `summable: \"\"`. Enables average queries (which return `(count, sum)` pairs the client divides) without forcing authors to think in terms of count + sum. Same on-disk layout as setting both underlying flags. If you set both `averageable` and `summable`, they must name the same property." + }, + "rangeAverageable": { + "type": "boolean", + "description": "Syntactic sugar: `rangeAverageable: true` is shorthand for `rangeCountable: true` + `rangeSummable: true`. Requires `averageable` to be set." + }, + "rankedCountable": { + "type": "boolean", + "description": "When true, the index's terminal property-name tree also carries an ordered secondary tree keyed by each group's document count, so \"top / bottom K groups by count\" queries are O(log n + k) with proofs. Requires `rangeCountable: true`. Adds the Count ranking axis only — the Sum and Avg axes are independent opt-ins (`rankedSummable` / `rankedAverageable`)." + }, + "rankedSummable": { + "type": "boolean", + "description": "When true, the index's terminal property-name tree also carries an ordered secondary tree keyed by each group's sum of the `summable` property, so \"top / bottom K groups by sum\" queries are O(log n + k) with proofs. Requires `rangeSummable: true`. Adds the Sum ranking axis only." + }, + "rankedAverageable": { + "type": "boolean", + "description": "When true, the index's terminal property-name tree also carries an ordered secondary tree keyed by each group's average (count + sum pair) of the `averageable` property, so \"top / bottom K groups by average\" queries are O(log n + k) with proofs. Requires `rangeAverageable: true` (which itself implies `rangeCountable` + `rangeSummable`). Adds the Avg ranking axis only — it does NOT imply `rankedCountable` or `rankedSummable`; each ranking axis costs its own secondary tree and is opted into separately." + } + }, + "required": [ + "properties", + "name" + ], + "dependentRequired": { + "rangeCountable": ["countable"], + "rangeSummable": ["summable"], + "rangeAverageable": ["averageable"] + }, + "$comment": "The ranked prerequisites are value-sensitive, unlike the range* rows above: `dependentRequired` fires on key *presence*, so listing them there would make an explicit `\"rankedCountable\": false` — a written-out opt-out, which the structural parser accepts as such — demand a `rangeCountable` the index does not need. The range* rows keep presence semantics because that is what they shipped with in v2 and changing them would move historical validation results.", + "allOf": [ + { + "if": { + "properties": { "rankedCountable": { "const": true } }, + "required": ["rankedCountable"] + }, + "then": { "required": ["rangeCountable"] } + }, + { + "if": { + "properties": { "rankedSummable": { "const": true } }, + "required": ["rankedSummable"] + }, + "then": { "required": ["rangeSummable"] } + }, + { + "if": { + "properties": { "rankedAverageable": { "const": true } }, + "required": ["rankedAverageable"] + }, + "then": { "required": ["rangeAverageable"] } + } + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 10 + }, + "signatureSecurityLevelRequirement": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "description": "Public key security level. 1 - Critical, 2 - High, 3 - Medium. If none specified, High level is used" + }, + "documentsKeepHistory": { + "type": "boolean", + "description": "True if the documents keep all their history, default is false" + }, + "keepsTransferHistory": { + "type": "boolean", + "description": "True if transfers of these documents are recorded in the document history system contract, default is false" + }, + "keepsPurchaseHistory": { + "type": "boolean", + "description": "True if purchases of these documents are recorded in the document history system contract, default is false" + }, + "keepsPricingHistory": { + "type": "boolean", + "description": "True if price updates on these documents are recorded in the document history system contract, default is false" + }, + "documentsMutable": { + "type": "boolean", + "description": "True if the documents are mutable, default is true" + }, + "canBeDeleted": { + "type": "boolean", + "description": "True if the documents can be deleted, default is true" + }, + "transferable": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "Transferable without a marketplace sell. 0 - Never, 1 - Always" + }, + "tradeMode": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "Built in marketplace system. 0 - None, 1 - Direct purchase (The user can buy the item without the need for an approval)" + }, + "creationRestrictionMode": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "Restrictions of document creation. 0 - No restrictions, 1 - Owner only, 2 - No creation (System Only)" + }, + "requiresIdentityEncryptionBoundedKey": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "Key requirements. 0 - Unique Non Replaceable, 1 - Multiple, 2 - Multiple with reference to latest key." + }, + "requiresIdentityDecryptionBoundedKey": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "Key requirements. 0 - Unique Non Replaceable, 1 - Multiple, 2 - Multiple with reference to latest key." + }, + "documentsCountable": { + "type": "boolean", + "description": "When true, the primary key tree uses a CountTree enabling O(1) total document count queries." + }, + "rangeCountable": { + "type": "boolean", + "description": "When true, the primary key tree uses a ProvableCountTree enabling range countable. Implies documentsCountable." + }, + "documentsSummable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Name of an integer document property aggregated into the primary-key SumTree (one sum per document type). Stores documents as `ItemWithSumItem` so the primary-key tree's root sum is the total of the named property across all docs of this type. Property must exist on the document type, be in `required`, and have an integer type. Composes with `documentsKeepHistory: true` — keep-history doctypes get a `SumTree` per-document subtree with a `ReferenceWithSumItem` on the `0`-key carrying the current version's value, so the doctype-level root aggregate reflects current versions only (historical versions don't double-count)." + }, + "rangeSummable": { + "type": "boolean", + "description": "When true, the primary key tree uses a ProvableSumTree (or ProvableCountProvableSumTree paired with rangeCountable: true) enabling O(log n) range-sum queries over the primary axis. Requires `documentsSummable` to be set. Rarely useful — range-sum on the primary key with no where-clause filter is unusual; most callers want per-index `rangeSummable` instead. Set this only when you need a provable global range-sum tree." + }, + "documentsAverageable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Syntactic sugar: `documentsAverageable: \"\"` is shorthand for `documentsCountable: true` + `documentsSummable: \"\"`. Enables doctype-wide average queries (returns `(count, sum)` the client divides) without authors having to compose the count + sum flags. Same on-disk layout. If you set both `documentsAverageable` and `documentsSummable`, they must name the same property. Composes with `documentsKeepHistory: true` via the per-doc SumTree + ReferenceWithSumItem layout described under `documentsSummable`." + }, + "rangeAverageable": { + "type": "boolean", + "description": "Syntactic sugar: `rangeAverageable: true` is shorthand for `rangeCountable: true` + `rangeSummable: true`. Requires `documentsAverageable` to be set. Same caveat as `rangeSummable` — rarely useful on the primary key; per-index `rangeAverageable` is what most callers want." + }, + "tokenCost": { + "type": "object", + "properties": { + "create": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "replace": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "delete": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "transfer": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "update_price": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "purchase": { + "$ref": "#/$defs/documentActionTokenCost" + } + }, + "additionalProperties": false + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/documentSchema" + } + ], + "unevaluatedProperties": false + }, + "properties": { + "$id": true, + "$ownerId": true, + "$revision": true, + "$createdAt": true, + "$updatedAt": true, + "$transferredAt": true, + "$createdAtBlockHeight": true, + "$updatedAtBlockHeight": true, + "$transferredAtBlockHeight": true, + "$createdAtCoreBlockHeight": true, + "$updatedAtCoreBlockHeight": true, + "$transferredAtCoreBlockHeight": true + }, + "propertyNames": { + "oneOf": [ + { + "type": "string", + "pattern": "^[a-zA-Z0-9-_]{1,64}$" + }, + { + "type": "string", + "enum": [ + "$id", + "$ownerId", + "$revision", + "$createdAt", + "$updatedAt", + "$transferredAt", + "$createdAtBlockHeight", + "$updatedAtBlockHeight", + "$transferredAtBlockHeight", + "$createdAtCoreBlockHeight", + "$updatedAtCoreBlockHeight", + "$transferredAtCoreBlockHeight" + ] + } + ] + }, + "minProperties": 1, + "maxProperties": 100 + }, + "transient": { + "type": "array", + "items": { + "type": "string" + } + }, + "additionalProperties": { + "type": "boolean", + "const": false + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "$comment": { + "type": "string" + }, + "description": { + "type": "string" + }, + "minProperties": { + "type": "integer", + "minimum": 0 + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + }, + "required": [ + "$schema", + "type", + "properties", + "additionalProperties" + ], + "dependentRequired": { + "rangeSummable": ["documentsSummable"], + "rangeAverageable": ["documentsAverageable"] + }, + "additionalProperties": false +} diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 06e04f2f69f..29f77e22a4a 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -89,6 +89,7 @@ use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValid #[cfg(feature = "validation")] use crate::validation::meta_validators::{ DOCUMENT_META_SCHEMA_V0, DOCUMENT_META_SCHEMA_V1, DOCUMENT_META_SCHEMA_V2, + DOCUMENT_META_SCHEMA_V3, }; #[cfg(feature = "validation")] use jsonschema::JSONSchema; @@ -101,12 +102,45 @@ use super::{ NOT_ALLOWED_SYSTEM_PROPERTIES, }; +/// RANKED: the extra index-property check a generation runs before the generic +/// index-key limits. +/// +/// Only generation 3 has one (the ranked axes tighten the key ceiling); every +/// earlier generation passes [`no_ranked_index_key_length_check`], which is the +/// exact no-op those generations perform today. Passing the check in rather +/// than branching on a version inside the shared core keeps the generation-only +/// rule — and the constants it is derived from — in the generation that owns it. +/// +/// This type and its no-op exist only for generation 3; without it there is no +/// per-property hook in the core at all. +pub(super) type RankedIndexKeyLengthCheck = + fn(&str, &Index, &str, &DocumentPropertyType, &PlatformVersion) -> Result<(), ProtocolError>; + +/// The [`RankedIndexKeyLengthCheck`] for a generation that has no ranking axes +/// to constrain — i.e. every generation whose index grammar rejects the +/// `ranked*` keywords outright. +pub(super) fn no_ranked_index_key_length_check( + _document_type_name: &str, + _index: &Index, + _index_property_name: &str, + _property_type: &DocumentPropertyType, + _platform_version: &PlatformVersion, +) -> Result<(), ProtocolError> { + Ok(()) +} + /// Everything the shared parsing steps need to know about *which* generation is /// running them. /// /// Each generation module constructs one of these from its own constants. The /// shared code never derives any of these fields from a platform version. +/// +/// The fields are split into two groups on purpose: the ranked-aggregate group +/// is exactly what a build without generation 3 does not need, so it can be +/// removed as a block. See the `RANKED` markers through this module for the +/// matching call sites. pub(super) struct ParserGeneration { + // ---- present in every generation ---- /// The `document_type_schema` table value to select the document /// meta-schema with. Read by the *driver*, not down here. pub document_type_schema_version: u16, @@ -124,6 +158,17 @@ pub(super) struct ParserGeneration { /// unknown `document_type_schema`. Differs per generation, so it is a /// parameter rather than a constant. pub meta_schema_method_name: &'static str, + + // ---- RANKED: generation-3 additions ---- + // Every field below exists only because generation 3 does. Drop them + // together with the `v3` module and the ranked arms they gate, and what is + // left is the parser as it stood before the ranked aggregates. + /// Whether the index grammar admits the `ranked*` keywords. Forwarded to + /// [`Index::try_from_value_map`], which rejects them as unknown keys when + /// this is `false`. + pub admit_ranked: bool, + /// See [`RankedIndexKeyLengthCheck`]. + pub ranked_index_key_length_check: RankedIndexKeyLengthCheck, } /// Reject a document type whose name is not a non-empty ASCII @@ -182,10 +227,11 @@ pub(super) fn validate_schema_depth_and_account_for_size( /// Pick the document meta-schema for a `document_type_schema` table value. /// -/// Every generation's own copy of this match named exactly meta-schemas 0, 1 -/// and 2 and reported `known_versions: [0, 1, 2]`, so the shared version is a -/// verbatim merge rather than a widening: `method_name` is the only thing that -/// ever differed between them. +/// This is a total function of the table value alone: the version tables pair +/// each `try_from_schema` generation with its `document_type_schema`, so the +/// pairing is enforced where it is authored, not re-checked down here. The +/// grammar a generation admits is gated by its own `ParserGeneration` flags, +/// not by which meta-schema validated the JSON. #[cfg(feature = "validation")] pub(super) fn select_document_meta_schema( document_type_schema_version: u16, @@ -195,10 +241,11 @@ pub(super) fn select_document_meta_schema( 0 => &*DOCUMENT_META_SCHEMA_V0, 1 => &*DOCUMENT_META_SCHEMA_V1, 2 => &*DOCUMENT_META_SCHEMA_V2, + 3 => &*DOCUMENT_META_SCHEMA_V3, version => { return Err(ProtocolError::UnknownVersionMismatch { method: method_name.to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, }) } @@ -730,12 +777,21 @@ fn parse_indices( index_values .iter() .map(|index_value| { - let index: Index = index_value - .to_map() - .map_err(consensus_or_protocol_value_error)? - .as_slice() - .try_into() - .map_err(consensus_or_protocol_data_contract_error)?; + // RANKED: whether the `ranked*` keywords are part of the + // grammar at all is the generation's own constant. When it + // is `false` they fall through to the unknown-key arm and + // are rejected with exactly the error the generation always + // produced — which is what `TryFrom<&[(Value, Value)]> for + // Index` does, so without generation 3 this whole call + // collapses back to `.as_slice().try_into()`. + let index: Index = Index::try_from_value_map( + index_value + .to_map() + .map_err(consensus_or_protocol_value_error)? + .as_slice(), + ctx.generation.admit_ranked, + ) + .map_err(consensus_or_protocol_data_contract_error)?; #[cfg(feature = "validation")] if ctx.full_validation { @@ -941,6 +997,26 @@ fn validate_index_properties( )) })?; + // RANKED: a ranking axis tightens the generic key + // limits below: the property's encoded value + // becomes the item key of a grovedb indexed + // tree, whose ordered secondary prefixes it + // with a sort key. Checked before the generic + // limits so a ranked index reports the bound + // that actually applies to it. System + // properties skip this the same way they skip + // the generic limits — every one of them + // encodes to a fixed 32 bytes or fewer. A + // generation without ranking axes passes the + // no-op check. + (ctx.generation.ranked_index_key_length_check)( + ctx.name, + index, + index_property.name.as_str(), + &property_definition.property_type, + ctx.platform_version, + )?; + // Validate indexed property type match &property_definition.property_type { // Array and objects aren't supported for indexing yet diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs index 47627961280..3bbc576e687 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs @@ -19,6 +19,7 @@ mod common; mod v0; mod v1; mod v2; +mod v3; const NOT_ALLOWED_SYSTEM_PROPERTIES: [&str; 1] = ["$id"]; @@ -88,9 +89,22 @@ impl DocumentType { validation_operations, platform_version, ), + 3 => DocumentType::try_from_schema_v3( + data_contract_id, + data_contract_system_version, + contract_config_version, + name, + schema, + schema_defs, + token_configurations, + data_contact_config, + full_validation, + validation_operations, + platform_version, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "try_from_schema".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, }), } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index 2ec7422a34f..41a56c52db9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -1,5 +1,5 @@ //! Document-type parser **generation 1** — protocol versions 9 through 11, -//! and the core that generation 2 (protocol version 12 onward) delegates to. +//! and the core that generation 2 (protocol versions 12 and 13) delegates to. //! //! The parsing steps themselves live in [`super::common`]; this module is the //! thin driver that names generation 1's grammar. @@ -34,8 +34,8 @@ impl DocumentTypeV1 { /// Generation 1 reaches it directly (`try_from_schema: 1`, protocol /// versions 9–11, `document_type_schema: 0`), and generation 2 reaches it /// by delegation from [`DocumentTypeV2::try_from_schema`] (`try_from_schema: - /// 2`, protocol version 12 onward, `document_type_schema` 1 at PV12 and 2 - /// from PV13 on). + /// 2`, protocol versions 12 and 13, `document_type_schema` 1 and 2 + /// respectively). /// /// That is why the `keeps*History` gate below is a real table read rather /// than a constant: the flags are absent for generation 1 and for PV12, and @@ -94,6 +94,11 @@ impl DocumentTypeV1 { admit_history: document_type_schema_version >= 2, admit_count_indexes: platform_version.protocol_version >= 12, meta_schema_method_name: "DocumentTypeV1::try_from_schema (document_type_schema)", + // RANKED: generation 1 predates the ranked aggregates entirely + // — its index grammar has no `ranked*` keywords, and it + // therefore has no ranked key ceiling to enforce. + admit_ranked: false, + ranked_index_key_length_check: common::no_ranked_index_key_length_check, }, platform_version, ) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs index 106b97e158e..5799e7d043c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs @@ -1,13 +1,12 @@ -//! Document-type parser **generation 2** — protocol version 12 onward. +//! Document-type parser **generation 2** — protocol versions 12 and 13. //! //! Generation 2 is generation 1 plus the doctype-level aggregate keywords //! (`documentsCountable` / `rangeCountable` / `documentsSummable` / //! `rangeSummable` and the `documentsAverageable` / `rangeAverageable` //! shorthands). Core parsing is delegated to the generation-1 entry point, //! which is where the `document_type_schema` read that distinguishes PV12 from -//! PV13 lives; the aggregate keywords themselves do not vary across the -//! protocol versions this generation serves, so this module passes no -//! version-dependent constants at all. +//! PV13 lives; the aggregate keywords themselves do not vary across the two, so +//! this module passes no version-dependent constants at all. use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::v1::DocumentTypeV1; @@ -130,10 +129,10 @@ mod tests { /// Generation-specific tests must pin a protocol version that actually /// selects their own generation: `PlatformVersion::latest()` silently /// retargets these tests onto a different parser generation and a - /// different document meta-schema whenever LATEST moves. PV13 is the first - /// protocol version whose `try_from_schema` selects generation 2 with - /// `document_type_schema: 2` — the meta-schema `latest()` resolves to - /// today, so behavior here is unchanged by the pin. + /// different document meta-schema whenever LATEST moves. PV13 is the + /// highest protocol version whose `try_from_schema` selects generation 2, + /// and its `document_type_schema` is 2 — the same meta-schema `latest()` + /// resolved to before generation 3 existed, so behavior here is unchanged. fn generation_2_platform_version() -> &'static PlatformVersion { PlatformVersion::get(13).expect("protocol version 13 exists") } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs new file mode 100644 index 00000000000..30ae2b94db9 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -0,0 +1,985 @@ +//! Document-type parser **generation 3** — protocol version 14 and later. +//! +//! Generation 3 is generation 2 plus the ranked index keywords +//! (`rankedCountable` / `rankedSummable` / `rankedAverageable`). +//! +//! It exists as its own generation — rather than as a version gate inside the +//! shipped ones — because that is what keeps a historical block from ever +//! picking up grammar that did not exist when it was produced: the dispatcher +//! only routes here from `try_from_schema: 3`, and the grammar this module +//! admits is spelled out below as literals rather than looked up in a version +//! table. +//! +//! The parsing steps themselves are shared with the earlier generations in +//! [`super::common`]. What stays here is what only generation 3 has: the ranked +//! index-key length ceilings, and the constants they are derived from. + +use crate::data_contract::config::DataContractConfig; +// Only the ranked key-length rule below names these, and it is validation-only. +#[cfg(feature = "validation")] +use crate::data_contract::document_type::index::Index; +#[cfg(feature = "validation")] +use crate::data_contract::document_type::property::DocumentPropertyType; +use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::data_contract::document_type::DocumentType; +use crate::data_contract::{TokenConfiguration, TokenContractPosition}; +use crate::validation::operations::ProtocolValidationOperation; +use crate::version::PlatformVersion; +use crate::ProtocolError; +use platform_value::{Identifier, Value}; +use std::collections::BTreeMap; + +#[cfg(feature = "validation")] +use crate::consensus::basic::data_contract::InvalidIndexedPropertyConstraintError; + +use super::common; + +/// grovedb's ceiling on the key of an entry stored directly under an +/// *indexed* tree's primary when the tree carries only the Count and/or Sum +/// axes (`grovedb::operations::indexed_tree::MAX_CIDX_ITEM_KEY_LEN`). +/// +/// Those two axes key their ordered secondary by `sort_key ‖ item_key` with +/// an 8-byte sort key, and Merk requires every key to stay below 256 bytes, +/// which leaves `255 - 8 = 247` bytes for the item key. GroveDB enforces it +/// on both write paths (the dedicated insert and the batch pre-state check), +/// so a document whose index key exceeded it would abort the whole batch. +#[cfg(feature = "validation")] +const MAX_RANKED_COUNT_SUM_INDEX_KEY_LENGTH: u16 = 247; + +/// The same ceiling for a ranked tree whose configured axes include Avg +/// (`grovedb::operations::indexed_tree::MAX_AVG_INDEXED_ITEM_KEY_LEN`). +/// +/// The avg secondary sorts on a 16-byte fixed-point key rather than an +/// 8-byte one, so the item key gets 8 fewer bytes: `255 - 16 = 239`. +#[cfg(feature = "validation")] +const MAX_RANKED_AVG_INDEX_KEY_LENGTH: u16 = 239; + +/// Worst-case bytes one character of a string property can occupy once it has +/// been encoded into an index key. +/// +/// `DocumentPropertyType::encode_value_for_tree_keys` stores a string as its +/// raw UTF-8 bytes, and a single UTF-8 scalar takes at most 4 bytes — which +/// is exactly the factor `DocumentPropertyType::max_byte_size` applies to +/// `maxLength`, and the factor the generic +/// [`MAX_INDEXED_STRING_PROPERTY_LENGTH`] was derived from +/// (`floor(255 / 4) = 63`). +#[cfg(feature = "validation")] +const INDEXED_STRING_WORST_CASE_BYTES_PER_CHARACTER: u16 = 4; + +/// The strictest item-key ceiling the index's declared ranking axes impose, +/// or `None` when the index declares no ranking axis at all (in which case +/// only the generic index-key limits apply). +/// +/// Avg wins when present because its wider sort key leaves the least room; +/// the three flags are independent, so an index may carry Avg alongside +/// Count and/or Sum and still has to satisfy the tightest of them. +#[cfg(feature = "validation")] +fn ranked_index_key_length_limit(index: &Index) -> Option { + if index.ranked_averageable { + Some(MAX_RANKED_AVG_INDEX_KEY_LENGTH) + } else if index.ranked_countable || index.ranked_summable { + Some(MAX_RANKED_COUNT_SUM_INDEX_KEY_LENGTH) + } else { + None + } +} + +/// Reject an index property whose *worst-case* encoded key would not fit +/// under the ceiling its index's ranking axes impose. +/// +/// A ranked index turns the property-name tree into a grovedb indexed tree +/// whose children — the per-group value trees, keyed by the encoded property +/// value — are mirrored into an ordered secondary keyed by +/// `sort_key ‖ item_key`. The generic index-key limits (255 encoded bytes for +/// a byte array, 63 characters for a string) were derived against Merk's bare +/// 256-byte key ceiling and are therefore too loose here by the width of the +/// sort key. Contracts must fail at registration rather than let a document +/// insert abort the block's batch on a key grovedb refuses to mirror. +/// +/// The worst case is derived exactly the way every other size-sensitive path +/// derives it, through [`DocumentPropertyType::max_byte_size`]: a string of +/// `maxLength` L characters can reach `4 * L` bytes, a byte array of +/// `maxItems` N reaches N, and every other indexable type has a fixed +/// encoding of at most 32 bytes (identifiers) that can never reach a ceiling +/// in the 239..=247 range. +#[cfg(feature = "validation")] +fn validate_ranked_index_property_key_length( + document_type_name: &str, + index: &Index, + index_property_name: &str, + property_type: &DocumentPropertyType, + platform_version: &PlatformVersion, +) -> Result<(), ProtocolError> { + let Some(limit) = ranked_index_key_length_limit(index) else { + return Ok(()); + }; + + // `None` is only produced by the array and object types, which the + // property-type check right after this one rejects outright with the + // error that actually explains the problem. + let Some(worst_case_key_length) = property_type.max_byte_size(platform_version)? else { + return Ok(()); + }; + + if worst_case_key_length <= limit { + return Ok(()); + } + + // The sort-key width is what the ceiling gives up against Merk's + // 255-byte maximum key, so it reads straight back out of the limit. + let sort_key_width = 255 - limit; + + let (constraint_name, reason) = match property_type { + DocumentPropertyType::String(_) => ( + "maxLength", + format!( + "should be less or equal {} on an index declaring a ranked axis: the index key \ + is the property's UTF-8 bytes (worst case {} bytes per character), and grovedb \ + caps a ranked group key at {} bytes because the ordered secondary is keyed by a \ + {}-byte sort key followed by the group key and Merk keys must stay below 256 \ + bytes", + limit / INDEXED_STRING_WORST_CASE_BYTES_PER_CHARACTER, + INDEXED_STRING_WORST_CASE_BYTES_PER_CHARACTER, + limit, + sort_key_width, + ), + ), + DocumentPropertyType::ByteArray(_) => ( + "maxItems", + format!( + "should be less or equal {limit} on an index declaring a ranked axis: grovedb \ + caps a ranked group key at {limit} bytes because the ordered secondary is keyed \ + by a {sort_key_width}-byte sort key followed by the group key and Merk keys must \ + stay below 256 bytes", + ), + ), + // Unreachable for every type the parser admits here — they all + // encode to at most 32 bytes — but stated rather than assumed so a + // future variable-width type cannot slip past unbounded. + _ => ( + "maximum encoded size", + format!( + "the property's worst-case encoded index key is {worst_case_key_length} bytes, \ + but an index declaring a ranked axis caps its group key at {limit} bytes (the \ + ordered secondary is keyed by a {sort_key_width}-byte sort key followed by the \ + group key and Merk keys must stay below 256 bytes)", + ), + ), + }; + + Err(ProtocolError::ConsensusError(Box::new( + InvalidIndexedPropertyConstraintError::new( + document_type_name.to_owned(), + index.name.to_owned(), + index_property_name.to_owned(), + constraint_name.to_string(), + reason, + ) + .into(), + ))) +} + +/// The [`common::RankedIndexKeyLengthCheck`] generation 3 runs on every indexed +/// property, resolved at compile time. +/// +/// Without the `validation` feature the shared core never reaches the check at +/// all (its whole `full_validation` block is compiled out), so the no-op is +/// exactly equivalent there — this alias exists only so the call site can pass +/// the check unconditionally. +#[cfg(feature = "validation")] +const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = + validate_ranked_index_property_key_length; +#[cfg(not(feature = "validation"))] +const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = + common::no_ranked_index_key_length_check; + +/// Parses a document type schema through the generation-3 grammar: the +/// generation-2 doctype-level aggregate keywords, plus the ranked index +/// keywords and the tighter index-key ceilings they impose. +/// +/// This parser is only reachable from protocol version 14+ (via +/// CONTRACT_VERSIONS_V6). +#[allow(clippy::too_many_arguments)] +fn try_from_schema_generation_3( + data_contract_id: Identifier, + data_contract_system_version: u16, + contract_config_version: u16, + name: &str, + schema: Value, + schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, + data_contact_config: &DataContractConfig, + full_validation: bool, + validation_operations: &mut impl Extend, + platform_version: &PlatformVersion, +) -> Result { + // Read the aggregate keywords before the core parser consumes `schema`. + let aggregates = common::parse_doctype_aggregate_keywords(&schema, name)?; + + let v1 = common::parse_document_type_core( + data_contract_id, + data_contract_system_version, + contract_config_version, + name, + schema, + schema_defs, + token_configurations, + data_contact_config, + full_validation, + validation_operations, + &common::ParserGeneration { + // Generation 3 exists if and only if `document_type_schema` is 3 — + // CONTRACT_VERSIONS_V6 is the only table that selects this parser, + // and it is the only table naming meta-schema v3. So every constant + // here is a property of the generation, not of a platform version, + // and none of them is read out of a table. + document_type_schema_version: 3, + // Meta-schema v3 carries the `keeps*History` flags forward from v2, + // so they are unconditionally part of this generation's grammar. + // This is the conditional that was dead in the copied core: at + // schema 3 the old `document_type_schema >= 2` could only be true. + admit_history: true, + // Count indexes arrived at PV12; every version selecting this + // generation is far past that boundary. + admit_count_indexes: true, + meta_schema_method_name: "DocumentType::try_from_schema_v3 (document_type_schema)", + // RANKED: the constants that make this generation 3. + admit_ranked: true, + ranked_index_key_length_check: RANKED_INDEX_KEY_LENGTH_CHECK, + }, + platform_version, + )?; + + let mut v2: DocumentTypeV2 = v1.into(); + common::apply_doctype_aggregates(&mut v2, aggregates, name)?; + + Ok(v2) +} + +impl DocumentType { + /// Dispatches to this module's generation-3 parser and wraps the result. + #[allow(clippy::too_many_arguments)] + pub(in crate::data_contract::document_type::class_methods) fn try_from_schema_v3( + data_contract_id: Identifier, + data_contract_system_version: u16, + contract_config_version: u16, + name: &str, + schema: Value, + schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, + data_contact_config: &DataContractConfig, + full_validation: bool, + validation_operations: &mut impl Extend, + platform_version: &PlatformVersion, + ) -> Result { + try_from_schema_generation_3( + data_contract_id, + data_contract_system_version, + contract_config_version, + name, + schema, + schema_defs, + token_configurations, + data_contact_config, + full_validation, + validation_operations, + platform_version, + ) + .map(DocumentType::V2) + } +} + +#[cfg(test)] +mod tests { + //! Ranked aggregate index keywords — parser-generation gating. + //! + //! The keywords live on the *index*, not the document type, but what + //! admits them is the parser generation the dispatcher selects + //! (`try_from_schema`: 2 at PV13, 3 at PV14), so the tests belong on this + //! parser. Two independent halves have to hold on the pre-PV14 side: + //! + //! * `full_validation: true` — the v2 meta-schema has + //! `additionalProperties: false` on index entries and rejects the key. + //! * `full_validation: false` — no meta-schema runs at all (check_tx, + //! cache warm-up, restore), so the *grammar* itself must not know the + //! keyword. That is the smuggling path, and the one worth pinning. + //! + //! The PV13 half of every test goes through the real dispatcher + //! (`DocumentType::try_from_schema`) rather than through anything in this + //! module, so what it exercises is generation 2 refusing the keys — the + //! cross-generation behavior, not this generation's internals. + use super::*; + use platform_value::platform_value; + + /// Parse through **this** generation, with the platform version and + /// validation mode spelled out. Used for the PV14 side, which has to be + /// exercised on both the meta-schema path (`full_validation: true`) and the + /// structural path (`full_validation: false`). + fn parse_with( + schema: Value, + platform_version: &PlatformVersion, + full_validation: bool, + ) -> Result { + let config = DataContractConfig::default_for_version(platform_version) + .expect("default config available on this platform version"); + try_from_schema_generation_3( + Identifier::new([1; 32]), + 1, + config.version(), + "test_doc", + schema, + None, + &BTreeMap::new(), + &config, + full_validation, + &mut vec![], + platform_version, + ) + } + + /// Parse through the **real dispatcher**, which picks the parser generation + /// out of the platform version's `try_from_schema` table value. At PV13 + /// that is generation 2; at PV14, this module. + fn parse_dispatched( + schema: Value, + platform_version: &PlatformVersion, + full_validation: bool, + ) -> Result { + let config = DataContractConfig::default_for_version(platform_version) + .expect("default config available on this platform version"); + DocumentType::try_from_schema( + Identifier::new([1; 32]), + 1, + config.version(), + "test_doc", + schema, + None, + &BTreeMap::new(), + &config, + full_validation, + &mut vec![], + platform_version, + ) + } + + /// A `review` doctype with one index over `restaurantId`, averageable on + /// `grade`, optionally carrying ranked keywords. Written so the v3 + /// meta-schema's prerequisite rules are satisfied: the ranked + /// `if`/`then` conditionals demand the literal range keys, and the + /// `dependentRequired` chain covers the rest + /// (`rangeAverageable` → `averageable`). + /// + /// `restaurantId` is capped at 32 characters — comfortably inside the + /// ranked key bound on every axis, so these grammar tests exercise the + /// keywords and nothing else. The bound itself is exercised by + /// [`ranked_review_schema_with_max_length`] below. + fn ranked_review_schema(ranked_keys: Vec<(&str, bool)>) -> Value { + ranked_review_schema_with_max_length(32, ranked_keys) + } + + /// [`ranked_review_schema`] with the indexed string property's + /// `maxLength` spelled out, for the ranked key-length boundary tests. + fn ranked_review_schema_with_max_length( + max_length: u32, + ranked_keys: Vec<(&str, bool)>, + ) -> Value { + let mut index_entry: Vec<(Value, Value)> = vec![ + ( + Value::Text("name".to_string()), + Value::Text("byRestaurant".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![Value::Map(vec![( + Value::Text("restaurantId".to_string()), + Value::Text("asc".to_string()), + )])]), + ), + ( + Value::Text("averageable".to_string()), + Value::Text("grade".to_string()), + ), + ( + Value::Text("rangeAverageable".to_string()), + Value::Bool(true), + ), + ]; + for (key, value) in ranked_keys { + index_entry.push((Value::Text(key.to_string()), Value::Bool(value))); + } + + Value::Map(vec![ + ( + Value::Text("type".to_string()), + Value::Text("object".to_string()), + ), + ( + Value::Text("properties".to_string()), + platform_value!({ + "restaurantId": { + "type": "string", + "maxLength": max_length, + "position": 0, + }, + "grade": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "position": 1, + }, + }), + ), + ( + Value::Text("required".to_string()), + Value::Array(vec![ + Value::Text("restaurantId".to_string()), + Value::Text("grade".to_string()), + ]), + ), + ( + Value::Text("additionalProperties".to_string()), + Value::Bool(false), + ), + ( + Value::Text("indices".to_string()), + Value::Array(vec![Value::Map(index_entry)]), + ), + ]) + } + + fn pv13() -> &'static PlatformVersion { + PlatformVersion::get(13).expect("protocol version 13 exists") + } + + /// Generation-specific tests must pin a protocol version that actually + /// selects their own generation: `pv14()` silently + /// retargets these tests onto a different parser generation and a + /// different document meta-schema whenever LATEST moves. PV14 is the + /// first protocol version whose `try_from_schema` selects generation 3. + fn pv14() -> &'static PlatformVersion { + PlatformVersion::get(14).expect("protocol version 14 exists") + } + + /// PV14 accepts the ranked keywords and carries them onto the parsed index + /// — both when parsed through this generation directly and when reached + /// the way production reaches it, through the dispatcher. The dispatcher + /// half is what pins that `try_from_schema: 3` actually routes here. + #[test] + fn ranked_keywords_accepted_at_pv14() { + let schema = ranked_review_schema(vec![("rankedAverageable", true)]); + let v2 = parse_with(schema.clone(), pv14(), true) + .expect("meta-schema v3 must accept the ranked index keywords"); + + let index = v2 + .indices + .get("byRestaurant") + .expect("index parsed under its name"); + assert!(index.ranked_averageable); + assert!(!index.ranked_countable); + assert!(!index.ranked_summable); + assert!(index.range_countable && index.range_summable); + + // Same schema, same platform version, through the real dispatcher. + let dispatched = parse_dispatched(schema, pv14(), true) + .expect("the dispatcher must route PV14 to a generation that accepts the keywords"); + let DocumentType::V2(dispatched) = dispatched else { + panic!("generation 3 produces a V2-shaped document type"); + }; + assert!( + dispatched + .indices + .get("byRestaurant") + .expect("index parsed under its name") + .ranked_averageable, + "dispatching at PV14 must reach generation 3, not an earlier generation" + ); + } + + /// PV13 + `full_validation`: generation 2's meta-schema rejects the unknown + /// index key outright. Routed through the dispatcher so it is genuinely + /// generation 2 doing the rejecting. + #[test] + fn ranked_keywords_rejected_at_pv13_under_full_validation() { + let schema = ranked_review_schema(vec![("rankedAverageable", true)]); + let result = parse_dispatched(schema, pv13(), true); + assert!( + result.is_err(), + "meta-schema v2 must reject rankedAverageable (additionalProperties: false)" + ); + } + + /// PV13 without `full_validation`: no meta-schema runs, so the structural + /// grammar has to do the rejecting. Both `true` and `false` are rejected — + /// the key's mere presence is what a pre-PV14 node refuses, and matching + /// that exactly is what keeps replay of historical blocks identical. + #[test] + fn ranked_keywords_rejected_at_pv13_without_full_validation() { + for key in ["rankedCountable", "rankedSummable", "rankedAverageable"] { + for value in [true, false] { + let schema = ranked_review_schema(vec![(key, value)]); + let result = parse_dispatched(schema, pv13(), false); + assert!( + result.is_err(), + "{key}: {value} must be rejected by the structural path at PV13 — the \ + meta-schema does not run here, so this is the only gate" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("unexpected property name"), + "PV13 must reject it as an unknown index key, exactly as a node without \ + the feature does; got {msg}" + ); + } + } + } + + /// Same schema, PV14, no full validation: accepted. Pins that the gate is + /// the parser *generation* and not the validation mode. + #[test] + fn ranked_keywords_accepted_at_pv14_without_full_validation() { + let schema = ranked_review_schema(vec![("rankedAverageable", true)]); + let v2 = parse_with(schema, pv14(), false) + .expect("PV14 structural parse must accept the ranked keywords"); + assert!( + v2.indices + .get("byRestaurant") + .expect("index parsed under its name") + .ranked_averageable + ); + } + + /// The meta-schema's ranked `if`/`then` conditionals are the declarative + /// half of the structural "ranking needs its range axis" rule: + /// `rankedCountable: true` without `rangeCountable` fails meta + /// validation at PV14. + #[test] + fn ranked_countable_without_range_countable_rejected_by_meta_schema() { + // `averageable` + `rangeAverageable` give the index its range axes in + // *effect*, but `rangeCountable` is not literally present, so the + // `if rankedCountable == true then require rangeCountable` + // conditional fails. + let schema = ranked_review_schema(vec![("rankedCountable", true)]); + let result = parse_with(schema, pv14(), true); + assert!( + result.is_err(), + "meta-schema v3 must demand rangeCountable alongside a true \ + rankedCountable" + ); + } + + /// An index over `restaurantId` carrying exactly one ranked keyword and + /// no aggregate layout whatsoever — the shape that separates "the key is + /// present" from "a ranking axis was asked for". + fn bare_ranked_index_schema(key: &str, value: bool) -> Value { + let index_entry = Value::Map(vec![ + ( + Value::Text("name".to_string()), + Value::Text("byRestaurant".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![Value::Map(vec![( + Value::Text("restaurantId".to_string()), + Value::Text("asc".to_string()), + )])]), + ), + (Value::Text(key.to_string()), Value::Bool(value)), + ]); + + Value::Map(vec![ + ( + Value::Text("type".to_string()), + Value::Text("object".to_string()), + ), + ( + Value::Text("properties".to_string()), + platform_value!({ + "restaurantId": { + "type": "string", + "maxLength": 63, + "position": 0, + }, + }), + ), + ( + Value::Text("required".to_string()), + Value::Array(vec![Value::Text("restaurantId".to_string())]), + ), + ( + Value::Text("additionalProperties".to_string()), + Value::Bool(false), + ), + ( + Value::Text("indices".to_string()), + Value::Array(vec![index_entry]), + ), + ]) + } + + /// The ranked prerequisites are **value-sensitive**. `dependentRequired` + /// fires on key *presence*, so expressing the opt-out explicitly + /// (`"rankedCountable": false`) would have been made to demand a range + /// axis the index never uses — a contract that says "no ranking here" + /// rejected for not declaring the machinery of a ranking it declined. + /// The structural parser reads `false` as "no ranking axis"; full + /// validation at PV14 must agree. + #[test] + fn ranked_flags_written_out_as_false_do_not_require_a_range_axis() { + for key in ["rankedCountable", "rankedSummable", "rankedAverageable"] { + let v2 = parse_with(bare_ranked_index_schema(key, false), pv14(), true).unwrap_or_else( + |e| panic!("`{key}: false` is an opt-out and must pass full validation: {e:?}"), + ); + + let index = v2 + .indices + .get("byRestaurant") + .expect("index parsed under its name"); + assert!( + !index.ranked_countable && !index.ranked_summable && !index.ranked_averageable, + "`{key}: false` must leave every ranking axis off" + ); + assert!( + !index.range_countable && !index.range_summable, + "`{key}: false` must not have conjured a range axis either" + ); + } + } + + /// The other half of the same rule: `true` without the matching range + /// axis is still refused under full validation, on every one of the three + /// axes. Making the prerequisite value-sensitive must not have made it + /// toothless. + #[test] + fn ranked_flags_set_true_still_require_their_range_axis() { + for key in ["rankedCountable", "rankedSummable", "rankedAverageable"] { + let result = parse_with(bare_ranked_index_schema(key, true), pv14(), true); + assert!( + result.is_err(), + "`{key}: true` with no range axis must be rejected under full validation" + ); + } + } + + /// Structural counterpart of the check above, on the path where no + /// meta-schema runs: `rankedCountable` with neither `countable` nor + /// `rangeCountable` in effect is rejected by the index parser itself. + #[test] + fn ranked_countable_without_range_countable_rejected_structurally() { + let schema = platform_value!({ + "type": "object", + "properties": { + "restaurantId": { + "type": "string", + "maxLength": 63, + "position": 0, + }, + }, + "required": ["restaurantId"], + "additionalProperties": false, + "indices": [{ + "name": "byRestaurant", + "properties": [{ "restaurantId": "asc" }], + "rankedCountable": true, + }], + }); + let result = parse_with(schema, pv14(), false); + assert!( + result.is_err(), + "rankedCountable with no range-count layout must be rejected structurally" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("rankedCountable") && msg.contains("rangeCountable"), + "error must name both flags; got {msg}" + ); + } + + // ----------------------------------------------------------------------- + // Ranked index key-length bounds + // ----------------------------------------------------------------------- + // + // A ranked index makes the property-name tree a grovedb *indexed* tree + // whose children — the per-group value trees, keyed by the encoded + // property value — are mirrored into an ordered secondary keyed by + // `sort_key ‖ item_key`. Merk keys must stay below 256 bytes, so grovedb + // caps the item key at `255 - sort_key_width`: 247 for the 8-byte Count + // and Sum sort keys, 239 for the 16-byte Avg one. The generic index-key + // limits (255 bytes / 63 characters) were derived against the bare 256 + // ceiling and are too loose by exactly the sort-key width, so the parser + // has to tighten them or a contract would register and then abort the + // batch of every document insert carrying an oversized key. + // + // The character bounds follow from the same worst case + // `DocumentPropertyType::max_byte_size` uses — 4 UTF-8 bytes per + // character, the very factor the generic 63 was derived from + // (`floor(255 / 4)`): + // + // Count/Sum: floor(247 / 4) = 61 characters (61 * 4 = 244 ✓, 62 * 4 = 248 ✗) + // Avg: floor(239 / 4) = 59 characters (59 * 4 = 236 ✓, 60 * 4 = 240 ✗) + + /// A doctype whose indexed `restaurantId` property is given verbatim and + /// whose single index carries `index_extras` on top of the mandatory + /// name/properties pair. `grade` is always present so the summable and + /// averageable layouts have an integer to aggregate. + fn ranked_bound_schema(indexed_property: Value, index_extras: Vec<(&str, Value)>) -> Value { + let mut index_entry: Vec<(Value, Value)> = vec![ + ( + Value::Text("name".to_string()), + Value::Text("byRestaurant".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![Value::Map(vec![( + Value::Text("restaurantId".to_string()), + Value::Text("asc".to_string()), + )])]), + ), + ]; + index_entry.extend( + index_extras + .into_iter() + .map(|(key, value)| (Value::Text(key.to_string()), value)), + ); + + Value::Map(vec![ + ( + Value::Text("type".to_string()), + Value::Text("object".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Map(vec![ + (Value::Text("restaurantId".to_string()), indexed_property), + ( + Value::Text("grade".to_string()), + platform_value!({ + "type": "integer", + "minimum": 0, + "maximum": 100, + "position": 1, + }), + ), + ]), + ), + ( + Value::Text("required".to_string()), + Value::Array(vec![ + Value::Text("restaurantId".to_string()), + Value::Text("grade".to_string()), + ]), + ), + ( + Value::Text("additionalProperties".to_string()), + Value::Bool(false), + ), + ( + Value::Text("indices".to_string()), + Value::Array(vec![Value::Map(index_entry)]), + ), + ]) + } + + fn string_property(max_length: u32) -> Value { + platform_value!({ + "type": "string", + "maxLength": max_length, + "position": 0, + }) + } + + fn byte_array_property(max_items: u32) -> Value { + platform_value!({ + "type": "array", + "byteArray": true, + "maxItems": max_items, + "position": 0, + }) + } + + /// Count axis only — an 8-byte sort key, so the 247-byte ceiling. + fn count_ranked_extras() -> Vec<(&'static str, Value)> { + vec![ + ("countable", Value::Text("countable".to_string())), + ("rangeCountable", Value::Bool(true)), + ("rankedCountable", Value::Bool(true)), + ] + } + + /// Sum axis only — also an 8-byte sort key, same 247-byte ceiling. + fn sum_ranked_extras() -> Vec<(&'static str, Value)> { + vec![ + ("summable", Value::Text("grade".to_string())), + ("rangeSummable", Value::Bool(true)), + ("rankedSummable", Value::Bool(true)), + ] + } + + /// Avg axis — a 16-byte sort key, so the tighter 239-byte ceiling. + fn avg_ranked_extras() -> Vec<(&'static str, Value)> { + vec![ + ("averageable", Value::Text("grade".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ] + } + + fn parse_bound(schema: Value) -> Result { + parse_with(schema, pv14(), true) + } + + /// Count-ranked and sum-ranked indexes share the 8-byte sort key, so both + /// stop the indexed string at 61 characters. + #[test] + fn ranked_count_and_sum_axes_bound_an_indexed_string_at_61_characters() { + for (axis, extras) in [ + ("rankedCountable", count_ranked_extras()), + ("rankedSummable", sum_ranked_extras()), + ] { + parse_bound(ranked_bound_schema(string_property(61), extras.clone())) + .unwrap_or_else(|e| panic!("{axis}: 61 * 4 = 244 <= 247 must be accepted: {e:?}")); + + let error = parse_bound(ranked_bound_schema(string_property(62), extras)) + .expect_err("62 * 4 = 248 > 247 must be rejected"); + let msg = format!("{error:?}"); + assert!( + msg.contains("maxLength") && msg.contains("61") && msg.contains("247"), + "{axis}: the error must name maxLength, the 61-character bound and the \ + 247-byte key ceiling it derives from; got {msg}" + ); + } + } + + /// The Avg axis's 16-byte sort key costs 8 more bytes, and the string + /// bound drops to 59 characters with it. + #[test] + fn ranked_avg_axis_bounds_an_indexed_string_at_59_characters() { + parse_bound(ranked_bound_schema( + string_property(59), + avg_ranked_extras(), + )) + .expect("59 * 4 = 236 <= 239 must be accepted"); + + let error = parse_bound(ranked_bound_schema( + string_property(60), + avg_ranked_extras(), + )) + .expect_err("60 * 4 = 240 > 239 must be rejected"); + let msg = format!("{error:?}"); + assert!( + msg.contains("maxLength") && msg.contains("59") && msg.contains("239"), + "the error must name maxLength, the 59-character bound and the 239-byte key \ + ceiling it derives from; got {msg}" + ); + } + + /// Avg is strictly tighter than Count/Sum, and an index carrying Avg + /// *alongside* the other axes has to satisfy the tightest of them: the + /// 60-character string that a count-only ranked index accepts is refused + /// the moment the Avg axis joins. + #[test] + fn the_avg_axis_bound_wins_when_several_ranking_axes_are_declared() { + // Count-only accepts it... + parse_bound(ranked_bound_schema( + string_property(60), + count_ranked_extras(), + )) + .expect("60 characters fits the count axis's 247-byte ceiling"); + + // ...and adding the Avg axis to the same index refuses it. + // The meta-schema's `dependentRequired` chain is literal, so + // `rangeCountable` has to bring `countable` along explicitly even + // though `averageable` already implies it in effect. + let mut both = avg_ranked_extras(); + both.push(("countable", Value::Text("countable".to_string()))); + both.push(("rangeCountable", Value::Bool(true))); + both.push(("rankedCountable", Value::Bool(true))); + let error = parse_bound(ranked_bound_schema(string_property(60), both)) + .expect_err("the Avg axis's 239-byte ceiling must win over the count axis's 247"); + let msg = format!("{error:?}"); + assert!( + msg.contains("239"), + "the strictest axis's ceiling must be the one reported; got {msg}" + ); + } + + /// Byte arrays are stored verbatim, so their bound is the ceiling itself: + /// 247 bytes on the count/sum axes, 239 once Avg is in play. + #[test] + fn ranked_axes_bound_an_indexed_byte_array_at_the_raw_ceiling() { + for (axis, extras, limit) in [ + ("rankedCountable", count_ranked_extras(), 247u32), + ("rankedSummable", sum_ranked_extras(), 247), + ("rankedAverageable", avg_ranked_extras(), 239), + ] { + parse_bound(ranked_bound_schema( + byte_array_property(limit), + extras.clone(), + )) + .unwrap_or_else(|e| panic!("{axis}: exactly {limit} bytes must be accepted: {e:?}")); + + let error = + match parse_bound(ranked_bound_schema(byte_array_property(limit + 1), extras)) { + Ok(_) => panic!("{axis}: {} bytes must be rejected", limit + 1), + Err(error) => error, + }; + let msg = format!("{error:?}"); + assert!( + msg.contains("maxItems") && msg.contains(&limit.to_string()), + "{axis}: the error must name maxItems and the {limit}-byte ceiling; got {msg}" + ); + } + } + + /// A property whose encoding is fixed and small — an integer group key is + /// 8 bytes — can never reach any ranked ceiling, on any axis. + #[test] + fn a_fixed_width_indexed_property_always_fits_every_ranked_axis() { + let integer_property = platform_value!({ + "type": "integer", + "minimum": 0_u32, + "maximum": 1_000_000_u32, + "position": 0, + }); + for (axis, extras) in [ + ("rankedCountable", count_ranked_extras()), + ("rankedSummable", sum_ranked_extras()), + ("rankedAverageable", avg_ranked_extras()), + ] { + parse_bound(ranked_bound_schema(integer_property.clone(), extras)).unwrap_or_else( + |e| panic!("{axis}: a fixed-width integer group key must always fit: {e:?}"), + ); + } + } + + /// The tightening is scoped to ranked indexes. Without a ranking axis the + /// generic limits are untouched — 63 characters and 255 bytes, both of + /// which a ranked index would refuse. + #[test] + fn a_non_ranked_index_keeps_the_generic_key_limits() { + parse_bound(ranked_bound_schema(string_property(63), vec![])) + .expect("63 characters is the generic string limit and must still pass"); + let error = parse_bound(ranked_bound_schema(string_property(64), vec![])) + .expect_err("64 characters exceeds the generic string limit"); + assert!( + format!("{error:?}").contains("63"), + "a non-ranked index must still report the generic 63-character limit" + ); + + parse_bound(ranked_bound_schema(byte_array_property(255), vec![])) + .expect("255 bytes is the generic byte-array limit and must still pass"); + let error = parse_bound(ranked_bound_schema(byte_array_property(256), vec![])) + .expect_err("256 bytes exceeds the generic byte-array limit"); + assert!( + format!("{error:?}").contains("255"), + "a non-ranked index must still report the generic 255-byte limit" + ); + + // The same aggregating (range) layout the ranking axes extend, minus + // the ranking: still the generic limits, since no secondary is keyed + // by these values. + let mut range_only = avg_ranked_extras(); + range_only.retain(|(key, _)| *key != "rankedAverageable"); + parse_bound(ranked_bound_schema(string_property(63), range_only)) + .expect("a range-averageable index without a ranking axis keeps the generic limit"); + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs index 2c5931bd36c..a7201eae4e3 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs @@ -27,6 +27,27 @@ use std::{collections::BTreeMap, convert::TryFrom}; pub mod random_index; +/// Index-level keyword opting the index's terminal property-name tree into the +/// **Count** ranking axis: an ordered secondary tree keyed by each group's +/// document count, so "top / bottom K groups by count" is O(log n + k) with a +/// proof. Requires `rangeCountable: true` on the same index. Only recognized +/// from document meta-schema v3 (protocol version 14); see +/// [`Index::try_from_value_map`]. +pub const RANKED_COUNTABLE: &str = "rankedCountable"; +/// Index-level keyword opting the index's terminal property-name tree into the +/// **Sum** ranking axis (ordered by each group's sum of the `summable` +/// property). Requires `rangeSummable` on the same index. Meta-schema v3+. +pub const RANKED_SUMMABLE: &str = "rankedSummable"; +/// Index-level keyword opting the index's terminal property-name tree into the +/// **Avg** ranking axis (ordered by each group's average of the `averageable` +/// property). Requires `rangeAverageable` semantics — i.e. both range axes. +/// Meta-schema v3+. +/// +/// Deliberately *not* sugar for the other two ranked flags: each ranking axis +/// costs its own secondary tree, so `rankedAverageable` adds the Avg axis and +/// nothing else. +pub const RANKED_AVERAGEABLE: &str = "rankedAverageable"; + #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] #[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] @@ -402,6 +423,60 @@ pub struct Index { /// picks the appropriate variant. #[cfg_attr(feature = "serde-conversion", serde(default))] pub range_summable: bool, + /// When `true`, this index's **terminal property-name tree** (the level + /// whose children are the last index property's value trees — i.e. one + /// child per group) is upgraded from its `Provable*` form to the matching + /// *indexed* tree, gaining an ordered secondary tree on the **Count** axis. + /// That secondary tree is keyed by each group's document count, so + /// "top / bottom K groups by count" is answerable in O(log n + k) with a + /// proof instead of enumerating every group. + /// + /// The indexed primary is a byte-compatible mirror of the tree it replaces, + /// so every existing range aggregate (`AggregateCountOnRange` &co.) keeps + /// working against it unchanged. + /// + /// `ranked_countable: true` requires [`Index::range_countable`] (which in + /// turn requires [`Index::countable`]): the ranking secondary is built from + /// the per-group counts the range-count layout already maintains. + /// + /// See `book/src/drive/document-ranked-trees.md` and + /// `book/src/drive/ranked-index-examples.md` for the worked example. + // + // `serde(default)`: added after the struct's serde shape was in the wild + // (see the note on `countable` above), so pre-existing JSON must still + // deserialize. + #[cfg_attr(feature = "serde-conversion", serde(default))] + pub ranked_countable: bool, + /// Sum-axis counterpart of [`Index::ranked_countable`]: the terminal + /// property-name tree gains an ordered secondary keyed by each group's sum + /// of the [`Index::summable`] property, making "top / bottom K groups by + /// sum" O(log n + k) with a proof. + /// + /// Requires [`Index::range_summable`] (which in turn requires + /// [`Index::summable`]). + /// + /// See `book/src/drive/document-ranked-trees.md` and + /// `book/src/drive/ranked-index-examples.md` for the worked example. + #[cfg_attr(feature = "serde-conversion", serde(default))] + pub ranked_summable: bool, + /// Average-axis counterpart of [`Index::ranked_countable`]: the terminal + /// property-name tree gains an ordered secondary keyed by each group's + /// average — stored as the (count, sum) pair the client divides, same + /// no-server-division rule the average query surface already follows. + /// + /// Requires `rangeAverageable` *semantics*: both [`Index::range_countable`] + /// and [`Index::range_summable`], however they were declared (via the + /// `rangeAverageable` / `averageable` sugar or the explicit + /// `countable` + `summable` + `rangeCountable` + `rangeSummable` longhand). + /// + /// The three ranking axes are **independent**: `ranked_averageable` does + /// NOT imply `ranked_countable` or `ranked_summable`. Each axis costs its + /// own ordered secondary tree, so each is opted into explicitly. + /// + /// See `book/src/drive/document-ranked-trees.md` and + /// `book/src/drive/ranked-index-examples.md` for the worked example. + #[cfg_attr(feature = "serde-conversion", serde(default))] + pub ranked_averageable: bool, } impl Index { @@ -562,7 +637,37 @@ impl Index { impl TryFrom<&[(Value, Value)]> for Index { type Error = DataContractError; + /// Parses an index definition **without** the ranked-aggregate grammar. + /// + /// This is the pre-meta-schema-v3 (pre protocol version 14) surface: the + /// three `ranked*` keywords are unknown property names here and are + /// rejected as such. Call sites that know the contract's + /// `document_type_schema` version must go through + /// [`Index::try_from_value_map`] instead so PV14+ contracts can use them. fn try_from(index_type_value_map: &[(Value, Value)]) -> Result { + Index::try_from_value_map(index_type_value_map, false) + } +} + +impl Index { + /// Parses an index definition from its `(key, value)` map form. + /// + /// `ranked_aggregates_allowed` mirrors `document_type_schema >= 3` (i.e. + /// document meta-schema v3, protocol version 14 and later). When it is + /// `false` the three ranked keywords (`rankedCountable`, `rankedSummable`, + /// `rankedAverageable`) are not part of the grammar at all: they fall + /// through to the unknown-key arm and are rejected with exactly the error a + /// pre-v14 node produced for them, so a non-validating parse (check_tx, + /// cache warm-up, restore) cannot smuggle a ranked index past a node whose + /// protocol version does not know how to lay one out on disk. + /// + /// The meta-schema is the other half of this gate — v2 rejects the keys via + /// `additionalProperties: false` — but it only runs under + /// `full_validation`, which is why the grammar itself is version-gated too. + pub fn try_from_value_map( + index_type_value_map: &[(Value, Value)], + ranked_aggregates_allowed: bool, + ) -> Result { // Decouple the map // It contains properties and a unique key // If the unique key is absent, then unique is false @@ -607,6 +712,14 @@ impl TryFrom<&[(Value, Value)]> for Index { // alongside `summable: "y"`) before the merge. let mut averageable: Option = None; let mut range_averageable = false; + // Ranking axes (meta-schema v3 / PV14+). Each one is an independent + // opt-in that adds one ordered secondary tree to the terminal + // property-name tree; unlike `averageable` / `rangeAverageable` there + // is no sugar relationship between them, so no explicit-vs-default + // tracking is needed — nothing ever promotes them. + let mut ranked_countable = false; + let mut ranked_summable = false; + let mut ranked_averageable = false; for (key_value, value_value) in index_type_value_map { let key = key_value.to_str()?; @@ -832,6 +945,36 @@ impl TryFrom<&[(Value, Value)]> for Index { "rangeAverageable value must be a boolean".to_string(), ))?; } + // The three ranking keywords are guarded on + // `ranked_aggregates_allowed`: when the contract's + // `document_type_schema` version predates v3 the guard fails, + // the arm doesn't match, and the key falls through to the + // unknown-property arm below — byte-identical to how a node + // without this feature rejects it. + RANKED_COUNTABLE if ranked_aggregates_allowed => { + ranked_countable = + value_value + .as_bool() + .ok_or(DataContractError::ValueWrongType( + "rankedCountable value must be a boolean".to_string(), + ))?; + } + RANKED_SUMMABLE if ranked_aggregates_allowed => { + ranked_summable = + value_value + .as_bool() + .ok_or(DataContractError::ValueWrongType( + "rankedSummable value must be a boolean".to_string(), + ))?; + } + RANKED_AVERAGEABLE if ranked_aggregates_allowed => { + ranked_averageable = + value_value + .as_bool() + .ok_or(DataContractError::ValueWrongType( + "rankedAverageable value must be a boolean".to_string(), + ))?; + } "properties" => { let properties = value_value @@ -981,6 +1124,105 @@ impl TryFrom<&[(Value, Value)]> for Index { )); } + // The ranking axes are checked after the `averageable` / + // `rangeAverageable` desugar above, so they see the *resolved* range + // flags: both the sugar form (`averageable` + `rangeAverageable`) and + // the explicit longhand (`countable` + `summable` + `rangeCountable` + + // `rangeSummable`) satisfy them identically. + // + // Each ranked flag adds one ordered secondary tree keyed by the + // aggregate the corresponding range axis already maintains per group, + // so the range axis is a hard prerequisite: without it the terminal + // property-name tree carries no per-group aggregate to rank by. + if ranked_countable && !range_countable { + return Err(DataContractError::InvalidContractStructure( + "rankedCountable requires rangeCountable: true; ranking groups by \ + count needs the per-group counts the range-count layout \ + maintains" + .to_string(), + )); + } + + if ranked_summable && !range_summable { + return Err(DataContractError::InvalidContractStructure( + "rankedSummable requires rangeSummable: true; ranking groups by \ + sum needs the per-group sums the range-sum layout maintains" + .to_string(), + )); + } + + // `rangeAverageable` semantics = both range axes (that is exactly what + // the sugar desugars into), which transitively pulls in `countable` + // and `summable` through the two checks above. + if ranked_averageable && !(range_countable && range_summable) { + return Err(DataContractError::InvalidContractStructure( + "rankedAverageable requires rangeAverageable semantics — both \ + rangeCountable and rangeSummable must be in effect (declare \ + `averageable` + `rangeAverageable`, or the explicit `countable` \ + + `summable` + `rangeCountable` + `rangeSummable` longhand); \ + ranking groups by average needs both the per-group counts and \ + the per-group sums" + .to_string(), + )); + } + + // Ranking orders groups (the distinct values of the index's last + // property) by a per-group aggregate. On a unique index every group + // holds at most one document, so every ranked ordering degenerates to + // a constant-per-group ordering that a plain range query already + // serves — while still paying for an indexed tree and its secondary + // maintenance on every write. Contested indexes are unique by + // construction (checked above). + if (ranked_countable || ranked_summable || ranked_averageable) && unique { + return Err(DataContractError::InvalidContractStructure( + "ranked aggregates are not supported on unique indexes: each \ + group of a unique index contains at most one document, so \ + there is nothing meaningful to rank" + .to_string(), + )); + } + + // Ranked aggregates are restricted to single-property indexes in + // this protocol version. Two reasons: a compound index whose prefix + // level also terminates an aggregating index would need its ranked + // terminal tree wrapped in a NonCounted/NotSummed shell, which the + // storage layer structurally rejects for indexed trees (the wrapper + // would neutralize the very aggregates the ranking indexes); and the + // ranked query surface deliberately has no equality-prefix routing + // yet. Both are relaxable at a future protocol version. + if (ranked_countable || ranked_summable || ranked_averageable) && index_properties.len() > 1 + { + return Err(DataContractError::InvalidContractStructure( + "ranked aggregates are only supported on single-property \ + indexes in this protocol version" + .to_string(), + )); + } + + // `nullSearchable: false` suppresses the terminal reference for a + // document that leaves the indexed property out — but the document + // index walker has already created that document's value tree by the + // time the terminal handler declines. Under a ranked index that value + // tree is an entry of a grovedb indexed primary, so the secondary + // mirrors it as a group whose aggregates are all zero: an + // authenticated TOP/BOTTOM answer would contain a group the index is + // supposed to exclude, with no document behind it. `nullSearchable` + // is only ever `false` when the contract says so explicitly — the + // default is `true` — and under `true` the null documents get their + // real reference and form a legitimate rankable group, which is the + // combination authors actually want. + if (ranked_countable || ranked_summable || ranked_averageable) && !null_searchable { + return Err(DataContractError::InvalidContractStructure( + "ranked aggregates are not supported with nullSearchable: false: a \ + document missing the indexed property still creates the null group's \ + value tree, but its reference is suppressed, so the ranking would \ + expose a phantom group with zero aggregates and no documents behind \ + it. Leave nullSearchable at its default (true), where documents with \ + a null value form a real, rankable group" + .to_string(), + )); + } + // if the index didn't have a name let's make one let name = name.unwrap_or_else(|| Alphanumeric.sample_string(&mut rand::thread_rng(), 24)); @@ -994,6 +1236,9 @@ impl TryFrom<&[(Value, Value)]> for Index { range_countable, summable, range_summable, + ranked_countable, + ranked_summable, + ranked_averageable, }) } } @@ -1061,6 +1306,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, } } @@ -1867,6 +2115,418 @@ mod tests { assert_eq!(index.summable.as_deref(), Some("score")); } + // ----------------------------------------------------------------------- + // Ranked aggregate index tests (meta-schema v3 / PV14 grammar) + // ----------------------------------------------------------------------- + + /// Helper: an index map on `score` with the supplied extra entries. + /// `properties` is always `[{score: asc}]` so the terminator — the level + /// the ranking axes attach to — is unambiguous. + fn ranked_index_map(extra: Vec<(&str, Value)>) -> Vec<(Value, Value)> { + let mut map: Vec<(Value, Value)> = vec![( + Value::Text("properties".to_string()), + Value::Array(vec![Value::Map(vec![( + Value::Text("score".to_string()), + Value::Text("asc".to_string()), + )])]), + )]; + map.extend( + extra + .into_iter() + .map(|(k, v)| (Value::Text(k.to_string()), v)), + ); + map + } + + /// All three ranking keywords parse as booleans on top of a fully + /// range-averageable index. + #[test] + fn test_index_try_from_all_three_ranked_keywords_parse() { + let index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedCountable", Value::Bool(true)), + ("rankedSummable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + let index = Index::try_from_value_map(index_map.as_slice(), true) + .expect("all three ranked keywords must parse when the grammar allows them"); + assert!(index.ranked_countable); + assert!(index.ranked_summable); + assert!(index.ranked_averageable); + // The underlying range axes are still what the sugar desugared into. + assert!(index.range_countable); + assert!(index.range_summable); + } + + /// Omitting the ranking keywords leaves all three axes off — the parser's + /// default matches the `serde(default)` on the struct fields. + #[test] + fn test_index_try_from_ranked_flags_default_false() { + let index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ]); + let index = Index::try_from_value_map(index_map.as_slice(), true) + .expect("index without ranked keywords must parse"); + assert!(!index.ranked_countable); + assert!(!index.ranked_summable); + assert!(!index.ranked_averageable); + } + + /// Ranked flags on compound indexes are a v1 scope restriction (wrapper + /// conflict under aggregating prefixes + no equality-prefix query + /// routing), rejected at parse time so the limitation is visible at + /// registration rather than at document-insert or query time. + #[test] + fn test_index_try_from_ranked_on_compound_index_rejected() { + let mut index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + // Turn the single-property fixture into a compound [region, score]. + index_map[0].1 = Value::Array(vec![ + Value::Map(vec![( + Value::Text("region".to_string()), + Value::Text("asc".to_string()), + )]), + Value::Map(vec![( + Value::Text("score".to_string()), + Value::Text("asc".to_string()), + )]), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "ranked flags on a compound index must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("single-property"), + "error must explain the single-property restriction; got {msg}" + ); + } + + /// Ranking is meaningless on a unique index: every group holds at most + /// one document, so ranked flags there are an authoring mistake and are + /// rejected rather than silently laying out an indexed tree. + #[test] + fn test_index_try_from_ranked_on_unique_index_rejected() { + let index_map = ranked_index_map(vec![ + ("unique", Value::Bool(true)), + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "ranked flags on a unique index must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("unique"), + "error must explain the unique-index restriction; got {msg}" + ); + } + + /// A ranking axis needs the matching range axis: the ordered secondary is + /// built from the per-group aggregate only the range layout maintains. + #[test] + fn test_index_try_from_ranked_countable_without_range_countable_rejected() { + // `countable` alone (no `rangeCountable`) is not enough. + let index_map = ranked_index_map(vec![ + ("countable", Value::Text("countable".to_string())), + ("rankedCountable", Value::Bool(true)), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "rankedCountable without rangeCountable must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("rankedCountable") && msg.contains("rangeCountable"), + "error must name both flags; got {msg}" + ); + } + + #[test] + fn test_index_try_from_ranked_summable_without_range_summable_rejected() { + let index_map = ranked_index_map(vec![ + ("summable", Value::Text("score".to_string())), + ("rankedSummable", Value::Bool(true)), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "rankedSummable without rangeSummable must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("rankedSummable") && msg.contains("rangeSummable"), + "error must name both flags; got {msg}" + ); + } + + /// `rankedAverageable` needs BOTH range axes. Only the count half here. + #[test] + fn test_index_try_from_ranked_averageable_without_range_summable_rejected() { + let index_map = ranked_index_map(vec![ + ("countable", Value::Text("countable".to_string())), + ("rangeCountable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "rankedAverageable with only the count range axis must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("rankedAverageable"), + "error must name rankedAverageable; got {msg}" + ); + } + + /// Only the sum half — symmetric rejection. + #[test] + fn test_index_try_from_ranked_averageable_without_range_countable_rejected() { + let index_map = ranked_index_map(vec![ + ("summable", Value::Text("score".to_string())), + ("rangeSummable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "rankedAverageable with only the sum range axis must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("rankedAverageable"), + "error must name rankedAverageable; got {msg}" + ); + } + + /// `rankedAverageable` accepted through the sugar form — `averageable` + + /// `rangeAverageable` desugar into both range axes before the ranking + /// checks run. + #[test] + fn test_index_try_from_ranked_averageable_with_sugar_form() { + let index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + let index = Index::try_from_value_map(index_map.as_slice(), true) + .expect("rankedAverageable on the averageable sugar form must parse"); + assert!(index.ranked_averageable); + assert!(index.countable.is_countable()); + assert_eq!(index.summable.as_deref(), Some("score")); + assert!(index.range_countable); + assert!(index.range_summable); + // Ranking axes are independent: the Avg opt-in adds no other axis. + assert!( + !index.ranked_countable && !index.ranked_summable, + "rankedAverageable must not imply rankedCountable / rankedSummable — each \ + ranking axis costs its own ordered secondary tree" + ); + } + + /// Same acceptance through the explicit longhand — `countable`, `summable` + /// and both range flags spelled out, no sugar keyword anywhere. The + /// structural rule is stated in terms of the resolved range axes, so both + /// spellings pass. + #[test] + fn test_index_try_from_ranked_averageable_with_explicit_longhand_form() { + let index_map = ranked_index_map(vec![ + ("countable", Value::Text("countable".to_string())), + ("summable", Value::Text("score".to_string())), + ("rangeCountable", Value::Bool(true)), + ("rangeSummable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ]); + let index = Index::try_from_value_map(index_map.as_slice(), true) + .expect("rankedAverageable on the explicit longhand form must parse"); + assert!(index.ranked_averageable); + assert!(index.range_countable); + assert!(index.range_summable); + assert!(!index.ranked_countable); + assert!(!index.ranked_summable); + } + + /// The keywords only accept booleans. + #[test] + fn test_index_try_from_ranked_non_boolean_rejected() { + for key in ["rankedCountable", "rankedSummable", "rankedAverageable"] { + let index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + (key, Value::Text("yes".to_string())), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!(result.is_err(), "{key} must reject a non-boolean value"); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains(key) && msg.contains("boolean"), + "error must name {key} and the boolean requirement; got {msg}" + ); + } + } + + /// With the pre-v3 grammar (`ranked_aggregates_allowed: false`, i.e. + /// `document_type_schema < 3`) the ranked keywords are not keywords at + /// all — they hit the unknown-property arm and produce exactly the error a + /// node without this feature produces. This is the gate that stops a + /// non-validating parse from smuggling a ranked index onto a pre-PV14 node. + #[test] + fn test_index_try_from_ranked_keys_rejected_when_grammar_disallows() { + for key in ["rankedCountable", "rankedSummable", "rankedAverageable"] { + for value in [Value::Bool(true), Value::Bool(false)] { + let index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + (key, value.clone()), + ]); + let result = Index::try_from_value_map(index_map.as_slice(), false); + assert!( + result.is_err(), + "{key}: {value:?} must be rejected when the ranked grammar is off" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("unexpected property name"), + "pre-v3 rejection must be the unknown-key error (byte-identical to a \ + node without the feature); got {msg}" + ); + } + } + } + + /// The `TryFrom` impl is the pre-v3 surface, so it rejects ranked keys. + #[test] + fn test_index_try_from_trait_impl_rejects_ranked_keys() { + let index_map = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedCountable", Value::Bool(true)), + ]); + let result = Index::try_from(index_map.as_slice()); + assert!( + result.is_err(), + "TryFrom is the pre-meta-schema-v3 grammar and must reject ranked keys" + ); + } + + /// The three range/ranked flag pairs that make a single-property index + /// legally rankable, one per axis. Used by the `nullSearchable` tests so + /// each axis is checked on its own rather than through the Avg superset. + fn ranked_axis_fixtures() -> Vec<(&'static str, Vec<(&'static str, Value)>)> { + vec![ + ( + "rankedCountable", + vec![ + ("countable", Value::Text("countable".to_string())), + ("rangeCountable", Value::Bool(true)), + ("rankedCountable", Value::Bool(true)), + ], + ), + ( + "rankedSummable", + vec![ + ("summable", Value::Text("score".to_string())), + ("rangeSummable", Value::Bool(true)), + ("rankedSummable", Value::Bool(true)), + ], + ), + ( + "rankedAverageable", + vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("rankedAverageable", Value::Bool(true)), + ], + ), + ] + } + + /// `nullSearchable: false` next to any ranking axis is refused at parse + /// time. The write path would still create the null group's value tree — + /// that happens in the index walker, before the terminal handler decides + /// to suppress the reference — so grovedb's secondary would carry a group + /// with zero aggregates and no documents behind it, and an authenticated + /// TOP/BOTTOM answer would include a group the index excludes. + #[test] + fn test_index_try_from_ranked_with_explicit_null_searchable_false_rejected() { + for (axis, mut extra) in ranked_axis_fixtures() { + extra.push(("nullSearchable", Value::Bool(false))); + let index_map = ranked_index_map(extra); + let result = Index::try_from_value_map(index_map.as_slice(), true); + assert!( + result.is_err(), + "{axis} with nullSearchable: false must be rejected" + ); + let msg = format!("{:?}", result.unwrap_err()); + assert!( + msg.contains("nullSearchable") && msg.contains("phantom"), + "error must name nullSearchable and explain the phantom group; got {msg}" + ); + } + } + + /// Omitting the key leaves the default (`true`), which is the shape the + /// rule permits: null documents get their real reference and form a + /// legitimate rankable group. + #[test] + fn test_index_try_from_ranked_without_null_searchable_key_accepted() { + for (axis, extra) in ranked_axis_fixtures() { + let index_map = ranked_index_map(extra); + let index = Index::try_from_value_map(index_map.as_slice(), true) + .unwrap_or_else(|e| panic!("{axis} with no nullSearchable key must parse: {e:?}")); + assert!( + index.null_searchable, + "{axis}: the absent key must still default to true" + ); + } + } + + /// Spelling out the default explicitly is equally fine — the rule is + /// about the resolved value, and `true` is the safe one. + #[test] + fn test_index_try_from_ranked_with_explicit_null_searchable_true_accepted() { + for (axis, mut extra) in ranked_axis_fixtures() { + extra.push(("nullSearchable", Value::Bool(true))); + let index_map = ranked_index_map(extra); + let index = Index::try_from_value_map(index_map.as_slice(), true).unwrap_or_else(|e| { + panic!("{axis} with an explicit nullSearchable: true must parse: {e:?}") + }); + assert!(index.null_searchable); + } + } + + /// The restriction is scoped to ranked indexes: `nullSearchable: false` + /// on an index without a ranking axis keeps working exactly as before, + /// including on the aggregating (range) layouts the ranking axes extend. + #[test] + fn test_index_try_from_non_ranked_with_null_searchable_false_still_accepted() { + let plain = ranked_index_map(vec![("nullSearchable", Value::Bool(false))]); + let index = Index::try_from_value_map(plain.as_slice(), true) + .expect("nullSearchable: false on a plain index must still parse"); + assert!(!index.null_searchable); + + let aggregating = ranked_index_map(vec![ + ("averageable", Value::Text("score".to_string())), + ("rangeAverageable", Value::Bool(true)), + ("nullSearchable", Value::Bool(false)), + ]); + let index = Index::try_from_value_map(aggregating.as_slice(), true) + .expect("nullSearchable: false on a range-averageable index must still parse"); + assert!(!index.null_searchable); + assert!(!index.ranked_averageable); + } + #[test] fn test_index_try_from_contested_without_unique_error() { let index_map: Vec<(Value, Value)> = vec![ @@ -2338,6 +2998,12 @@ mod json_convertible_tests { range_countable: true, summable: Some("price".to_string()), range_summable: true, + // Ranking axes set asymmetrically on purpose: the round-trip has to + // prove each flag survives independently, which a uniform + // all-true / all-false fixture cannot. + ranked_countable: true, + ranked_summable: false, + ranked_averageable: true, } } @@ -2364,6 +3030,9 @@ mod json_convertible_tests { "range_countable": true, "summable": "price", "range_summable": true, + "ranked_countable": true, + "ranked_summable": false, + "ranked_averageable": true, }) ); let recovered = Index::from_json(json).expect("from_json"); @@ -2382,7 +3051,8 @@ mod json_convertible_tests { /// JSON serialized before the count (#3623) and sum (#3661) fields existed /// must still deserialize — the four new fields default (NotCountable / /// false / None / false). Without `serde(default)` this fails with - /// "missing field `countable`". + /// "missing field `countable`". The three ranking flags (added later still, + /// for the PV14 ranked-aggregate grammar) default the same way. #[test] fn index_deserializes_pre_count_sum_json() { use crate::serialization::JsonConvertible; @@ -2398,6 +3068,37 @@ mod json_convertible_tests { assert!(!recovered.range_countable); assert_eq!(recovered.summable, None); assert!(!recovered.range_summable); + assert!(!recovered.ranked_countable); + assert!(!recovered.ranked_summable); + assert!(!recovered.ranked_averageable); + } + + /// JSON that predates only the *ranking* flags (it already carries the + /// count and sum fields) must also deserialize, with all three ranking + /// axes defaulting to `false`. This is the shape any `Index` serialized + /// between the sum work and the PV14 ranked grammar has. + #[test] + fn index_deserializes_pre_ranked_json() { + use crate::serialization::JsonConvertible; + let old_json = serde_json::json!({ + "name": "byOwnerAndPrice", + "properties": [{"name": "ownerId", "ascending": true}], + "unique": false, + "null_searchable": true, + "contested_index": serde_json::Value::Null, + "countable": "countable", + "range_countable": true, + "summable": "price", + "range_summable": true, + }); + let recovered = Index::from_json(old_json).expect("pre-ranked JSON must deserialize"); + assert_eq!(recovered.countable, IndexCountability::Countable); + assert!(recovered.range_countable); + assert_eq!(recovered.summable.as_deref(), Some("price")); + assert!(recovered.range_summable); + assert!(!recovered.ranked_countable); + assert!(!recovered.ranked_summable); + assert!(!recovered.ranked_averageable); } #[test] diff --git a/packages/rs-dpp/src/data_contract/document_type/index/random_index.rs b/packages/rs-dpp/src/data_contract/document_type/index/random_index.rs index b9b1050a3f6..a352c935e86 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index/random_index.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index/random_index.rs @@ -64,6 +64,9 @@ impl Index { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }) } } diff --git a/packages/rs-dpp/src/data_contract/document_type/index_level/find_first_change.rs b/packages/rs-dpp/src/data_contract/document_type/index_level/find_first_change.rs index 6e7c77cb332..80b37ef390f 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index_level/find_first_change.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index_level/find_first_change.rs @@ -1,14 +1,13 @@ //! Pairwise diff helpers for [`IndexLevel`] trees, used by //! `validate_update` to surface the first index-path where a -//! count- or sum-affecting flag differs between an old and a new -//! contract version. +//! count-, sum- or ranking-affecting flag differs between an old and +//! a new contract version. //! -//! Both flags (count and sum) drive grovedb tree-variant choice at -//! contract creation, so toggling either after creation would -//! require rebuilding the index tree and is rejected. The error -//! messages include from→to values so contract authors can see -//! *what* changed at the rejected path, not just *that* something -//! did. +//! All of them drive grovedb tree-variant choice at contract +//! creation, so toggling any after creation would require rebuilding +//! the index tree and is rejected. The error messages include from→to +//! values so contract authors can see *what* changed at the rejected +//! path, not just *that* something did. use super::IndexLevel; @@ -108,4 +107,56 @@ impl IndexLevel { None } + + /// Ranking counterpart of [`Self::find_first_countability_change`] and + /// [`Self::find_first_summability_change`]. Recursively finds the first + /// index path where any of the three ranking axes (`ranked_countable`, + /// `ranked_summable`, `ranked_averageable`) differs between two + /// `IndexLevel` trees. + /// + /// All three live in one helper because the Avg axis is neither a + /// count-only nor a sum-only property, and because they are decided + /// together: the set of declared axes picks the indexed tree variant + /// (`ProvableCountIndexedTree` / `ProvableSumIndexedTree` / + /// `ProvableCountProvableSumIndexedTree(axes)`) and its ordered secondaries + /// at contract creation. Toggling any one of them would require rebuilding + /// the secondaries for every existing group, so all three are immutable — + /// same reasoning, and same error, as the count and sum flags. + /// + /// Returns `None` if all three properties are the same everywhere. + #[cfg(feature = "validation")] + pub(super) fn find_first_ranked_change(&self, new: &IndexLevel) -> Option { + if let (Some(old_info), Some(new_info)) = + (&self.has_index_with_type, &new.has_index_with_type) + { + if old_info.ranked_countable != new_info.ranked_countable { + return Some(format!( + "(ranked_countable: {} -> {})", + old_info.ranked_countable, new_info.ranked_countable, + )); + } + if old_info.ranked_summable != new_info.ranked_summable { + return Some(format!( + "(ranked_summable: {} -> {})", + old_info.ranked_summable, new_info.ranked_summable, + )); + } + if old_info.ranked_averageable != new_info.ranked_averageable { + return Some(format!( + "(ranked_averageable: {} -> {})", + old_info.ranked_averageable, new_info.ranked_averageable, + )); + } + } + + for (key, old_sub) in &self.sub_index_levels { + if let Some(new_sub) = new.sub_index_levels.get(key) { + if let Some(inner_path) = old_sub.find_first_ranked_change(new_sub) { + return Some(format!("{} -> {}", key, inner_path)); + } + } + } + + None + } } diff --git a/packages/rs-dpp/src/data_contract/document_type/index_level/mod.rs b/packages/rs-dpp/src/data_contract/document_type/index_level/mod.rs index 36d4a24c4ba..77295ca0eae 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index_level/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index_level/mod.rs @@ -79,6 +79,28 @@ pub struct IndexLevelTypeInfo { /// together promote the tree to a `ProvableCountSumTree`. Requires /// `summable.is_some()`. pub range_summable: bool, + /// Whether this index ranks its groups by **count**. When `true`, the + /// property-name level (the level *above* this terminating level, whose + /// keys are the terminator property's distinct values — one child tree per + /// group) is upgraded from `ProvableCountTree` / `ProvableCountSumTree` to + /// the matching *indexed* tree carrying an ordered secondary on the Count + /// axis, so "top / bottom K groups by count" is O(log n + k) with a proof. + /// + /// The indexed primary mirrors the tree it replaces byte for byte, so the + /// existing range-count reads are unaffected. Requires `range_countable`. + pub ranked_countable: bool, + /// Sum-axis counterpart of `ranked_countable`: the same property-name level + /// gains an ordered secondary keyed by each group's running sum. Requires + /// `range_summable`. + pub ranked_summable: bool, + /// Average-axis counterpart of `ranked_countable`: the same property-name + /// level gains an ordered secondary keyed by each group's (count, sum) + /// average. Requires both `range_countable` and `range_summable`. + /// + /// The three ranking axes are independent — this one does not imply the + /// other two. The set of axes declared here is what the rs-drive write path + /// turns into the indexed tree's axis list. + pub ranked_averageable: bool, } impl IndexType { @@ -268,6 +290,17 @@ impl IndexLevel { range_countable: index.range_countable, summable: index.summable.clone(), range_summable: index.range_summable, + // The ranking axes live on the same terminating level + // as the range axes they extend: this is the level + // named after the index's LAST property, whose children + // are that property's value trees (one per group). The + // rs-drive write path reads them off the very same + // `IndexLevelTypeInfo` it already consults for + // `range_countable` / `range_summable` when it picks + // the property-name tree variant. + ranked_countable: index.ranked_countable, + ranked_summable: index.ranked_summable, + ranked_averageable: index.ranked_averageable, }); } } @@ -347,6 +380,25 @@ impl IndexLevel { ); } + // Same check on the ranking surface (`ranked_countable` / + // `ranked_summable` / `ranked_averageable`). All three are checked + // together in one helper rather than folded into the count and sum + // helpers above because the Avg axis straddles both — it is neither a + // count-only nor a sum-only property — and because the set of ranking + // axes is what determines the indexed tree's axis list, which is + // committed into the parent hash at contract creation. Adding or + // removing an axis after the fact would require rebuilding the ordered + // secondaries for every existing group, so the whole set is immutable. + if let Some(ranked_change_path) = self.find_first_ranked_change(new_indices) { + return SimpleConsensusValidationResult::new_with_error( + DataContractInvalidIndexDefinitionUpdateError::new( + document_type_name.to_string(), + ranked_change_path, + ) + .into(), + ); + } + SimpleConsensusValidationResult::new() } } @@ -375,6 +427,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -406,6 +461,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![ @@ -422,6 +480,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }, Index { name: "test2".to_string(), @@ -436,6 +497,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }, ]; @@ -476,6 +540,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }, Index { name: "test2".to_string(), @@ -490,6 +557,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }, ]; @@ -506,6 +576,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -544,6 +617,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -565,6 +641,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -609,6 +688,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -624,6 +706,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -662,6 +747,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -677,6 +765,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -715,6 +806,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -730,6 +824,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -768,6 +865,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -806,6 +906,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -821,6 +924,9 @@ mod tests { range_countable: true, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -859,6 +965,9 @@ mod tests { range_countable: true, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -874,6 +983,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -918,6 +1030,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -939,6 +1054,9 @@ mod tests { range_countable: true, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -983,6 +1101,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let new_indices = vec![Index { @@ -1004,6 +1125,9 @@ mod tests { range_countable: false, summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, }]; let old_index_structure = @@ -1023,4 +1147,217 @@ mod tests { )] if e.index_path() == "first -> second -> (countable: NotCountable -> Countable)" ); } + + // ----------------------------------------------------------------------- + // Ranked aggregate axes (meta-schema v3 / PV14) + // ----------------------------------------------------------------------- + + /// Fully range-averageable index on `[first, second]` with the supplied + /// ranking axes. Compound on purpose: the ranking flags must land on the + /// level of the LAST property, which is where the value trees (one per + /// group) hang and therefore where rs-drive picks the indexed tree variant. + fn ranked_index( + ranked_countable: bool, + ranked_summable: bool, + ranked_averageable: bool, + ) -> Index { + Index { + name: "compound".to_string(), + properties: vec![ + IndexProperty { + name: "first".to_string(), + ascending: true, + }, + IndexProperty { + name: "second".to_string(), + ascending: true, + }, + ], + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::Countable, + range_countable: true, + summable: Some("score".to_string()), + range_summable: true, + ranked_countable, + ranked_summable, + ranked_averageable, + } + } + + /// The ranking flags must be carried onto the terminating level — the one + /// named after the index's LAST property — next to the range flags they + /// extend, because that is the `IndexLevelTypeInfo` the rs-drive write path + /// consults when it picks the property-name tree variant. + #[test] + fn ranked_flags_land_on_the_terminal_property_level() { + let platform_version = PlatformVersion::latest(); + let indices = vec![ranked_index(true, false, true)]; + + let structure = IndexLevel::try_from_indices(&indices, "test", platform_version) + .expect("failed to create index level"); + + let first = structure + .sub_levels() + .get("first") + .expect("first level exists"); + assert!( + first.has_index_with_type().is_none(), + "no index terminates at the prefix level, so it carries no type info" + ); + + let second = first + .sub_levels() + .get("second") + .expect("second (terminal) level exists"); + let info = second + .has_index_with_type() + .expect("the index terminates at the last property level"); + assert!(info.ranked_countable); + assert!(!info.ranked_summable); + assert!(info.ranked_averageable); + // The range axes the ranking extends live on the same info. + assert!(info.range_countable); + assert!(info.range_summable); + } + + #[test] + fn should_return_invalid_result_if_ranked_countable_changed_from_false_to_true() { + let platform_version = PlatformVersion::latest(); + let document_type_name = "test"; + + let old_indices = vec![ranked_index(false, false, false)]; + let new_indices = vec![ranked_index(true, false, false)]; + + let old_index_structure = + IndexLevel::try_from_indices(&old_indices, document_type_name, platform_version) + .expect("failed to create old index level"); + let new_index_structure = + IndexLevel::try_from_indices(&new_indices, document_type_name, platform_version) + .expect("failed to create new index level"); + + let result = old_index_structure.validate_update(document_type_name, &new_index_structure); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::DataContractInvalidIndexDefinitionUpdateError(e) + )] if e.index_path() == "first -> second -> (ranked_countable: false -> true)" + ); + } + + #[test] + fn should_return_invalid_result_if_ranked_summable_changed_from_true_to_false() { + let platform_version = PlatformVersion::latest(); + let document_type_name = "test"; + + let old_indices = vec![ranked_index(false, true, false)]; + let new_indices = vec![ranked_index(false, false, false)]; + + let old_index_structure = + IndexLevel::try_from_indices(&old_indices, document_type_name, platform_version) + .expect("failed to create old index level"); + let new_index_structure = + IndexLevel::try_from_indices(&new_indices, document_type_name, platform_version) + .expect("failed to create new index level"); + + let result = old_index_structure.validate_update(document_type_name, &new_index_structure); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::DataContractInvalidIndexDefinitionUpdateError(e) + )] if e.index_path() == "first -> second -> (ranked_summable: true -> false)" + ); + } + + #[test] + fn should_return_invalid_result_if_ranked_averageable_changed() { + let platform_version = PlatformVersion::latest(); + let document_type_name = "test"; + + let old_indices = vec![ranked_index(false, false, false)]; + let new_indices = vec![ranked_index(false, false, true)]; + + let old_index_structure = + IndexLevel::try_from_indices(&old_indices, document_type_name, platform_version) + .expect("failed to create old index level"); + let new_index_structure = + IndexLevel::try_from_indices(&new_indices, document_type_name, platform_version) + .expect("failed to create new index level"); + + let result = old_index_structure.validate_update(document_type_name, &new_index_structure); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::DataContractInvalidIndexDefinitionUpdateError(e) + )] if e.index_path() == "first -> second -> (ranked_averageable: false -> true)" + ); + } + + #[test] + fn should_pass_if_ranked_flags_unchanged_on_update() { + let platform_version = PlatformVersion::latest(); + let document_type_name = "test"; + + let old_indices = vec![ranked_index(true, true, true)]; + + let old_index_structure = + IndexLevel::try_from_indices(&old_indices, document_type_name, platform_version) + .expect("failed to create old index level"); + let new_index_structure = old_index_structure.clone(); + + let result = old_index_structure.validate_update(document_type_name, &new_index_structure); + + assert!(result.is_valid()); + } + + /// Adding a brand-new ranked index on update follows the same policy as + /// adding any other new index: rejected, because the new structure is not a + /// subset of the old one. Pinned here so the ranking work can't be read as + /// carving out an exception. + #[test] + fn should_return_invalid_result_if_new_ranked_index_is_added_on_update() { + let platform_version = PlatformVersion::latest(); + let document_type_name = "test"; + + let old_indices = vec![Index { + name: "test".to_string(), + properties: vec![IndexProperty { + name: "test".to_string(), + ascending: false, + }], + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::NotCountable, + range_countable: false, + summable: None, + range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, + }]; + + let mut new_indices = old_indices.clone(); + new_indices.push(ranked_index(true, false, false)); + + let old_index_structure = + IndexLevel::try_from_indices(&old_indices, document_type_name, platform_version) + .expect("failed to create old index level"); + let new_index_structure = + IndexLevel::try_from_indices(&new_indices, document_type_name, platform_version) + .expect("failed to create new index level"); + + let result = old_index_structure.validate_update(document_type_name, &new_index_structure); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::DataContractInvalidIndexDefinitionUpdateError(e) + )] if e.index_path() == "first" + ); + } } diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs index e79860e7fa6..755cd1519a4 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs @@ -1588,4 +1588,108 @@ mod tests { ); } } + + /// The ranking axes are index-level, so `validate_config` — which covers + /// the *doctype*-level count / sum flags just above — is deliberately not + /// where they are enforced. They ride the index-structure comparison + /// (`IndexLevel::validate_update`) that `validate_update_v0` runs right + /// after the config check. These tests exercise the PUBLIC dispatcher so + /// that routing is pinned, not just the helper in isolation. + mod validate_update_ranked_indices { + use super::*; + use std::collections::BTreeMap; + + /// `review` doctype, one averageable index over `restaurantId`, with + /// `rankedAverageable` set to the supplied value. + fn document_type_with_ranked_index( + ranked_averageable: bool, + platform_version: &PlatformVersion, + ) -> DocumentType { + let schema = platform_value!({ + "type": "object", + "properties": { + // 32 rather than the generic 63-character index limit: + // an index declaring a ranking axis bounds its group key + // more tightly (59 characters on the Avg axis), and both + // halves of these tests have to build the same doctype + // shape with only `rankedAverageable` differing. + "restaurantId": { + "type": "string", + "maxLength": 32, + "position": 0, + }, + "grade": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "position": 1, + }, + }, + "required": ["restaurantId", "grade"], + "additionalProperties": false, + "indices": [{ + "name": "byRestaurant", + "properties": [{ "restaurantId": "asc" }], + "averageable": "grade", + "rangeAverageable": true, + "rankedAverageable": ranked_averageable, + }], + }); + + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create a default config"); + + DocumentType::try_from_schema( + Identifier::random(), + 1, + config.version(), + "review", + schema, + None, + &BTreeMap::new(), + &config, + true, + &mut Vec::new(), + platform_version, + ) + .expect("failed to create document type") + } + + #[test] + fn should_return_invalid_result_when_ranked_averageable_is_changed() { + let platform_version = PlatformVersion::latest(); + let old = document_type_with_ranked_index(false, platform_version); + let new = document_type_with_ranked_index(true, platform_version); + + let result = old + .as_ref() + .validate_update(new.as_ref(), platform_version) + .expect("validate_update should not error"); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + crate::consensus::basic::BasicError::DataContractInvalidIndexDefinitionUpdateError(e) + )] if e.index_path() == "restaurantId -> (ranked_averageable: false -> true)" + ); + } + + #[test] + fn should_pass_when_ranked_averageable_is_unchanged() { + let platform_version = PlatformVersion::latest(); + let old = document_type_with_ranked_index(true, platform_version); + let new = document_type_with_ranked_index(true, platform_version); + + let result = old + .as_ref() + .validate_update(new.as_ref(), platform_version) + .expect("validate_update should not error"); + + assert!( + result.is_valid(), + "an unchanged ranked index must not be rejected, got {:?}", + result.errors + ); + } + } } diff --git a/packages/rs-dpp/src/data_contract/factory/v0/mod.rs b/packages/rs-dpp/src/data_contract/factory/v0/mod.rs index 6931d83badd..9f463884a75 100644 --- a/packages/rs-dpp/src/data_contract/factory/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/factory/v0/mod.rs @@ -354,7 +354,7 @@ mod tests { ); let contract_value = platform_value::to_value( - &DataContract::try_from_platform_versioned( + DataContract::try_from_platform_versioned( result.data_contract().to_owned(), false, &mut vec![], diff --git a/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs b/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs index 6dce1253630..f1522e5f888 100644 --- a/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs @@ -112,7 +112,24 @@ impl DataContractInSerializationFormat { ) { for index_value in index_values { if let Ok(index_value_map) = index_value.to_map() { - if let Ok(index) = Index::try_from(index_value_map.as_slice()) { + // Same ranked-keyword gate the document type parser + // applies (`document_type_schema >= 3`, i.e. meta + // schema v3 / protocol version 14). Without it a + // PV14 index carrying `rankedCountable` &co. would + // fail to parse here and be billed nothing, while + // the identical index parses fine during + // validation — the fee must cover every index the + // contract actually registers. + if let Ok(index) = Index::try_from_value_map( + index_value_map.as_slice(), + platform_version + .dpp + .contract_versions + .document_type_versions + .schema + .document_type_schema + >= 3, + ) { let base_index_fee = if index.contested_index.is_some() { fee_version.document_type_base_contested_index_registration_fee } else if index.unique { diff --git a/packages/rs-dpp/src/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index 4b3e835fada..cfb737411a0 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -48,6 +48,10 @@ lazy_static! { "../../../schema/meta_schemas/document/v2/document-meta.json" )) .expect("v1 document meta-schema JSON must be valid"); + static ref DOCUMENT_META_JSON_V3: Value = serde_json::from_str::(include_str!( + "../../../schema/meta_schemas/document/v3/document-meta.json" + )) + .expect("v3 document meta-schema JSON must be valid"); pub static ref DRAFT_202012_META_SCHEMA: JSONSchema = JSONSchema::options() .with_draft(Draft::Draft202012) @@ -240,4 +244,59 @@ lazy_static! { .compile(&DOCUMENT_META_JSON_V2) .expect("Invalid data contract schema"); + // Compiled version of document meta schema v3 + // Introduced for protocol version 14 (contract-level ranked aggregates). + // v2 plus the three index-level ranking keywords — `rankedCountable`, + // `rankedSummable`, `rankedAverageable` — and the `dependentRequired` + // rows tying each to its range axis. Hosting them on a schema only v14+ + // contracts validate against leaves v13 validation untouched: under v2 + // the keys still fail `additionalProperties: false` on an index entry. + pub static ref DOCUMENT_META_SCHEMA_V3: JSONSchema = JSONSchema::options() + .with_keyword( + "byteArray", + |_, _, _| Ok(Box::new(ByteArrayKeyword)), + ) + .with_patterns_regex_engine(RegexEngine::Regex(RegexOptions { + size_limit: Some(5 * (1 << 20)), + ..Default::default() + })) + .should_ignore_unknown_formats(false) + .should_validate_formats(true) + .with_draft(Draft::Draft202012) + .with_document( + "https://json-schema.org/draft/2020-12/meta/applicator".to_string(), + DRAFT202012_APPLICATOR.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/core".to_string(), + DRAFT202012_CORE.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/unevaluated".to_string(), + DRAFT202012_UNEVALUATED.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/validation".to_string(), + DRAFT202012_VALIDATION.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/meta-data".to_string(), + DRAFT202012_META_DATA.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/format-annotation".to_string(), + DRAFT202012_FORMAT_ANNOTATION.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/content".to_string(), + DRAFT202012_CONTENT.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/schema".to_string(), + DRAFT202012.clone(), + ) + .to_owned() + .compile(&DOCUMENT_META_JSON_V3) + .expect("Invalid data contract schema"); + } diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index ebab4fd1a61..23069f0158e 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -82,7 +82,7 @@ derive_more = { version = "1.0", features = ["from", "deref", "deref_mut"] } async-trait = "0.1.77" console-subscriber = { version = "0.4", optional = true } bls-signatures = { git = "https://github.com/dashpay/bls-signatures", rev = "0842b17583888e8f46c252a4ee84cdfd58e0546f", optional = true } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } nonempty = "0.11" # Shielded-pool snapshot needs raw RocksDB SstFileWriter + ingest_external_file_cf # bindings, and blake3 for the snapshot-file checksum. @@ -107,7 +107,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ drive = { path = "../rs-drive", features = ["fixtures-and-mocks"] } drive-proof-verifier = { path = "../rs-drive-proof-verifier" } strategy-tests = { path = "../strategy-tests" } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", features = ["client"] } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", features = ["client"] } assert_matches = "1.5.0" drive-abci = { path = ".", features = ["testing-config", "mocks", "shielded_test_data"] } bls-signatures = { git = "https://github.com/dashpay/bls-signatures", rev = "0842b17583888e8f46c252a4ee84cdfd58e0546f" } @@ -121,8 +121,8 @@ integer-encoding = { version = "4.0.0" } # For dump_only_default_and_aux_cfs_under_shielded_subtree_prefix — same # subtree-prefix algorithm grovedb uses internally. -grovedb-path = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } -grovedb-storage = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } +grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } +grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } [features] default = ["bls-signatures"] diff --git a/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/mod.rs b/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/mod.rs index 6381f695b2d..8fe31b340b8 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/mod.rs @@ -1,28 +1,64 @@ //! Versioned dispatcher for the //! `compute_aggregate_mode_and_check_limit` helper. //! -//! The helper picks the `(group_by × where)` execution mode for -//! `SELECT COUNT` / `SUM` / `AVG` against the v1 query surface, then -//! enforces the per-mode `accepts_limit()` contract. The routing rules -//! it embeds are part of the query contract clients see on the wire — -//! a future change to which `(group_by × where_clauses)` shapes are -//! accepted (e.g. adding a third group_by field, or relaxing the -//! "Aggregate rejects limit" rule) becomes consensus-visible because -//! the dispatcher runs on every v1 query request. Versioning it lets -//! later protocol bumps adjust the routing table without breaking -//! older nodes' replay of historical traffic. +//! The helper decides how a `SELECT COUNT` / `SUM` / `AVG` request +//! against the v1 query surface is executed: as one of the four +//! `(group_by × where)` grouped modes, or — from feature version 1 — +//! as a *ranked* request when the request orders by the aggregate it +//! selects (`GROUP BY p ORDER BY `). Routing +//! here only asks whether the `order_by` names that aggregate; the +//! direction, the limit's bounds and the offset are drive's call and +//! are made in `detect_ranked_mode`. It also enforces the per-mode +//! `accepts_limit()` contract on the grouped path. +//! +//! The routing rules it embeds are part of the query contract clients +//! see on the wire — a change to which `(group_by × where_clauses × +//! order_by)` shapes are accepted becomes consensus-visible because the +//! dispatcher runs on every v1 query request. Versioning it lets later +//! protocol bumps adjust the routing table without breaking older +//! nodes' replay of historical traffic, and is what keeps a +//! mixed-version network in agreement across the ranked-query +//! activation: protocol version 13 and earlier select v0, which has no +//! ranked path at all, while protocol version 14 selects v1 and answers +//! ranked queries. //! //! Lives next to the v1 query handler (the only call site today) and //! is dispatched via the `DriveAbciDocumentQueryHelperVersions` slot //! in `PlatformVersion`. mod v0; +mod v1; use crate::error::query::QueryError; use dpp::version::PlatformVersion; -use drive::query::{CountMode, WhereClause}; +use drive::query::{CountMode, HavingClause, OrderClause, SelectProjection, WhereClause}; + +/// What the helper decided a request should execute as. +/// +/// `Grouped` is the pre-existing outcome — the `(group_by × where)` +/// mode the count / sum / average executors take. `Ranked` is the +/// feature-version-1 addition: the request orders by the aggregate it +/// selects and belongs to `Drive::execute_document_ranked_request`, +/// which re-derives `(axis, direction, k, offset)` from the same +/// `(select, group_by, order_by, limit, offset)` inputs. +/// +/// The ranked variant deliberately carries no data. Everything the +/// ranked dispatcher needs is already owned by the handler (the +/// decoded `select`, `group_by`, `order_by`, `limit` and `offset`), and +/// duplicating the resolved axis / direction / `k` here would create a +/// second place where the ranking grammar is interpreted — drive owns +/// that grammar (`detect_ranked_mode`), and a routing-layer copy could +/// disagree with it after a version bump. Routing decides *where* a +/// request goes; drive decides what it means. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum AggregateRouting { + /// Non-ranked aggregate: execute with this `(group_by × where)` mode. + Grouped(CountMode), + /// Ranked aggregate: execute through the ranked (top-k) surface. + Ranked, +} -/// Compute the `(group_by × where)` mode for SELECT COUNT / SUM / AVG. +/// Decide how a `SELECT COUNT` / `SUM` / `AVG` request executes. /// /// All three aggregate functions share the same SQL-shape contract /// (empty group_by → Aggregate; one-field group_by → GroupByIn or @@ -31,34 +67,56 @@ use drive::query::{CountMode, WhereClause}; /// GroupByCompound). The `function_name` arg ("COUNT" / "SUM" / "AVG") /// is woven into rejection messages for clarity. /// -/// Also runs the `accepts_limit()` check: `Aggregate` and `GroupByIn` -/// can't honor a caller-supplied limit; rejects with -/// `QuerySyntaxError::InvalidLimit` if one is set. +/// On the grouped path it also runs the `accepts_limit()` check: +/// `Aggregate` and `GroupByIn` can't honor a caller-supplied limit and +/// reject with `QuerySyntaxError::InvalidLimit` if one is set. The +/// ranked path deliberately does *not* check `limit` here — on that +/// path `limit` is the ranking's `k`, and drive owns both its +/// mandatoriness and its `1 ..= 100` bound; duplicating those rules at +/// the routing layer would mean two error messages for one contract. /// /// Routes through `platform_version.drive_abci.query.document_query_helpers.compute_aggregate_mode_and_check_limit`. +#[allow(clippy::too_many_arguments)] pub(super) fn compute_aggregate_mode_and_check_limit( + select: &SelectProjection, group_by: &[String], where_clauses: &[WhereClause], + order_by: &[OrderClause], limit: Option, + having: &[HavingClause], function_name: &str, platform_version: &PlatformVersion, -) -> Result { +) -> Result { match platform_version .drive_abci .query .document_query_helpers .compute_aggregate_mode_and_check_limit { + // v0 predates the ranked surface entirely: it has no `order_by` + // input because ordering played no part in its routing + // decision, and passing one would be the first step toward + // changing what an already-shipped protocol version accepts. 0 => v0::compute_aggregate_mode_and_check_limit_v0( group_by, where_clauses, limit, + having, + function_name, + ), + 1 => v1::compute_aggregate_mode_and_check_limit_v1( + select, + group_by, + where_clauses, + order_by, + limit, + having, function_name, ), version => Err(QueryError::Drive(drive::error::Error::Drive( drive::error::drive::DriveError::UnknownVersionMismatch { method: "compute_aggregate_mode_and_check_limit".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }, ))), diff --git a/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v0/mod.rs b/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v0/mod.rs index f1c8f35965c..9486be8ad92 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v0/mod.rs @@ -1,21 +1,41 @@ -//! v0 of `compute_aggregate_mode_and_check_limit`. +//! v0 of `compute_aggregate_mode_and_check_limit` — the protocol +//! version 13 routing table. //! //! Original routing table extracted verbatim from //! `query/document_query/v1/mod.rs` so the v1 cutover is a pure code //! move with no semantic change. See the dispatcher's module-level //! docstring for the versioning rationale. +//! +//! The blanket `HAVING` rejection below moved in from the same place +//! for the same reason: it was an unversioned gate in +//! `validate_and_route`, and "reject every non-empty HAVING" is +//! precisely this table's behavior. Keeping it here rather than in the +//! dispatcher means an older node's routing decisions are described by +//! one function rather than two, and the ranked activation is a pure +//! version selection. +use super::AggregateRouting; use crate::error::query::QueryError; use crate::query::document_query::v1::not_yet_implemented; use drive::error::query::QuerySyntaxError; -use drive::query::{CountMode, WhereClause, WhereOperator}; +use drive::query::{CountMode, HavingClause, WhereClause, WhereOperator}; pub(super) fn compute_aggregate_mode_and_check_limit_v0( group_by: &[String], where_clauses: &[WhereClause], limit: Option, + having: &[HavingClause], function_name: &str, -) -> Result { +) -> Result { + // v0 has no ranked surface: every non-empty HAVING is refused, + // whatever its shape. The wording signals future capability + // rather than a malformed request — a client can keep the + // request unchanged and it starts working once the network + // upgrades to a protocol version whose table selects v1. + if !having.is_empty() { + return Err(not_yet_implemented("HAVING clause")); + } + let is_range_op = |op: WhereOperator| { matches!( op, @@ -89,5 +109,5 @@ pub(super) fn compute_aggregate_mode_and_check_limit_v0( return Err(QueryError::Query(QuerySyntaxError::InvalidLimit(reason))); } - Ok(mode) + Ok(AggregateRouting::Grouped(mode)) } diff --git a/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v1/mod.rs b/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v1/mod.rs new file mode 100644 index 00000000000..11eb264b1c9 --- /dev/null +++ b/packages/rs-drive-abci/src/query/document_query/v1/compute_aggregate_mode_and_check_limit/v1/mod.rs @@ -0,0 +1,121 @@ +//! v1 of `compute_aggregate_mode_and_check_limit` — the protocol +//! version 14 routing table, which adds the ranked +//! (`ORDER BY LIMIT n OFFSET m`) path. +//! +//! Behaviour splits on the **`order_by` clause**: +//! +//! - the request names the selected aggregate in a single `ORDER BY` +//! clause, and has a `GROUP BY` — route to the ranked executor. +//! - anything else — delegate to [`v0`](super::v0) verbatim. Every +//! non-ranked request routes exactly as it did before the upgrade, +//! which is what makes v1 safe to activate: only requests a v13 node +//! would have *rejected* behave differently. +//! +//! ## What "routing-level shape" means here +//! +//! This function answers one question — *is this request bound for the +//! ranked executor?* — and nothing else. The full ranked grammar +//! (which axis, which direction, `1 ≤ limit ≤ 100`, no `where`, no +//! `having`, no `start_at`, exactly one `group_by` property, and +//! whether the contract's index actually declares the axis) is owned by +//! drive's +//! `drive_document_ranked_query::mode_detection::detect_ranked_mode` +//! and its index picker, and is itself versioned there. +//! +//! Re-validating any of that here would create two sources of truth +//! for one grammar: the SDK's proof helpers call drive's validator +//! directly (no abci in the path), so a routing-layer copy that +//! drifted would make the server accept or reject requests the client +//! disagrees about. The checks below are the minimum needed to *route*: +//! +//! 1. **`group_by` is non-empty.** A ranking ranks groups; with no +//! grouping there is exactly one implicit group and `ORDER BY +//! ` is a no-op rather than a ranking. +//! 2. **Exactly one `order_by` clause, naming the selected +//! aggregate.** This is the discriminator, and it has to be exact: +//! `SELECT COUNT(*) GROUP BY brand ORDER BY brand` is a perfectly +//! ordinary grouped count and must keep routing to the grouped +//! path, while `… ORDER BY $count` is the ranked one. Matching on +//! the field name is what keeps those two apart. +//! 3. **`having` is empty on the ranked path.** Boolean per-group +//! predicates are not evaluated at all yet, and combining one with +//! an aggregate ordering is a distinct (also unimplemented) +//! capability — see the rejection below. +//! +//! Note what is *not* checked: the limit's bounds, its presence at +//! all, `where` / `start_at` presence, `offset`'s value. Those all +//! reach drive and come back as `Error::Query(...)`, which the ranked +//! dispatcher maps onto `QueryError::Query` — a client-visible +//! rejection, not an internal error. + +use super::v0::compute_aggregate_mode_and_check_limit_v0; +use super::AggregateRouting; +use crate::error::query::QueryError; +use crate::query::document_query::v1::not_yet_implemented; +use drive::query::{ + HavingClause, OrderClause, SelectProjection, WhereClause, RANKED_COUNT_ORDER_KEY, +}; + +/// The `order_by` field name that names `select`'s aggregate: +/// the projection's own field for `SUM(f)` / `AVG(f)`, and the +/// [`RANKED_COUNT_ORDER_KEY`] sentinel for `COUNT(*)`. +/// +/// Mirrors drive's private `ranked_order_key`. The duplication is +/// deliberate and safe in a way the rest of the grammar would not be: +/// this is a *name*, pinned by a shared public constant, not a rule. +/// A drift would be a compile-time constant mismatch, not a silent +/// semantic disagreement — and calling into drive here would mean +/// building a `DocumentRankedMode` before the contract has even been +/// fetched. +fn aggregate_order_key(select: &SelectProjection) -> &str { + if select.field.is_empty() { + RANKED_COUNT_ORDER_KEY + } else { + select.field.as_str() + } +} + +pub(super) fn compute_aggregate_mode_and_check_limit_v1( + select: &SelectProjection, + group_by: &[String], + where_clauses: &[WhereClause], + order_by: &[OrderClause], + limit: Option, + having: &[HavingClause], + function_name: &str, +) -> Result { + let orders_by_the_aggregate = match order_by { + [only] => only.field == aggregate_order_key(select), + _ => false, + }; + + if group_by.is_empty() || !orders_by_the_aggregate { + // Not a ranked shape: identical routing to v0, including its + // `accepts_limit()` enforcement and its blanket HAVING + // rejection. + return compute_aggregate_mode_and_check_limit_v0( + group_by, + where_clauses, + limit, + having, + function_name, + ); + } + + // Ranked shape. `having` is refused here rather than downstream + // because the two rejections say different things: drive's is + // about the ranked *executor* and only fires once a contract has + // been fetched, while this one keeps the `not_yet_implemented` + // contract the rest of the v1 surface uses for capabilities that + // are wired on the wire but not in the server. + if !having.is_empty() { + return Err(not_yet_implemented(&format!( + "boolean HAVING combined with `ORDER BY {}(…)` (ranking reads a pre-sorted \ + axis secondary, so filtering groups first would mean walking every group to \ + test the predicate — the exact cost the ranked surface exists to avoid)", + function_name + ))); + } + + Ok(AggregateRouting::Ranked) +} diff --git a/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs b/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs index a61152b6171..fa5c4da140b 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs @@ -30,17 +30,15 @@ use crate::error::query::QueryError; use dapi_grpc::platform::v0::get_documents_request::{ document_field_value, get_documents_request_v1::{select, Select as ProtoSelect}, - having_aggregate, having_clause, having_ranking, order_clause, - DocumentFieldValue as ProtoDocumentFieldValue, HavingAggregate as ProtoHavingAggregate, - HavingClause as ProtoHavingClause, HavingRanking as ProtoHavingRanking, + having_aggregate, having_clause, order_clause, DocumentFieldValue as ProtoDocumentFieldValue, + HavingAggregate as ProtoHavingAggregate, HavingClause as ProtoHavingClause, OrderClause as ProtoOrderClause, WhereClause as ProtoWhereClause, WhereOperator as ProtoWhereOperator, }; use dpp::platform_value::Value; use drive::query::{ - HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRanking, - HavingRankingKind, HavingRightOperand, OrderClause, SelectFunction, SelectProjection, - WhereClause, WhereOperator, + HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, + OrderClause, SelectFunction, SelectProjection, WhereClause, WhereOperator, }; /// Map a wire-level [`ProtoWhereOperator`] discriminant onto @@ -201,14 +199,14 @@ pub(super) fn order_clauses_from_proto( clauses.into_iter().map(order_clause_from_proto).collect() } -// The `having_*_from_proto` family below is currently dead code: -// the v1 handler short-circuits non-empty HAVING with -// `not_yet_implemented` before decoding (see `query_documents_v1`). -// The helpers stay in tree so HAVING execution can land with just -// the gate-flip + a single call into `having_clauses_from_proto`; -// no separate decoder needs to be written then. The -// `#[allow(dead_code)]` is per-function rather than module-wide so -// any future addition outside this family still trips the lint. +// The `having_*_from_proto` family below decodes clauses the server +// then refuses: `having` evaluation is not implemented, so every +// non-empty HAVING is rejected at routing. Decoding still runs first +// (see `query_documents_v1`) so wire-malformed clauses surface as +// `InvalidArgument` rather than being masked by the capability +// rejection. The inner helpers keep a per-function +// `#[allow(dead_code)]` — rather than module-wide — so any future +// addition outside this family still trips the lint. /// Map a wire [`having_aggregate::Function`] discriminant onto /// drive's [`HavingAggregateFunction`]. Unknown discriminants are @@ -231,38 +229,6 @@ fn having_function_from_proto(function: i32) -> Result Result { - let proto = having_ranking::Kind::try_from(kind).map_err(|_| { - QueryError::InvalidArgument(format!( - "unknown HavingRanking.Kind discriminant: {} (valid values: 0..=3, see \ - `get_documents_request::having_ranking::Kind`)", - kind - )) - })?; - Ok(match proto { - having_ranking::Kind::Min => HavingRankingKind::Min, - having_ranking::Kind::Max => HavingRankingKind::Max, - having_ranking::Kind::Top => HavingRankingKind::Top, - having_ranking::Kind::Bottom => HavingRankingKind::Bottom, - }) -} - -/// Map a wire [`ProtoHavingRanking`] onto drive's [`HavingRanking`]. -/// The `kind` ↔ `n` consistency check (e.g. `n` required for -/// `Top` / `Bottom`, forbidden on `Min` / `Max`) runs inside the -/// evaluator when HAVING execution lands; this converter only -/// enforces that the proto shape is well-formed. -#[allow(dead_code)] -fn having_ranking_from_proto(ranking: ProtoHavingRanking) -> Result { - Ok(HavingRanking { - kind: having_ranking_kind_from_proto(ranking.kind)?, - n: ranking.n, - }) -} - /// Map a wire [`having_clause::Operator`] discriminant onto /// drive's [`HavingOperator`]. Same error contract as /// [`having_function_from_proto`]. @@ -309,10 +275,17 @@ fn having_aggregate_from_proto( /// Map a wire [`ProtoHavingClause`] onto drive's structured /// [`HavingClause`]. Errors surface as /// [`QueryError::InvalidArgument`] for any wire-level -/// malformation: unknown discriminant on the aggregate function, -/// operator, or ranking kind; missing aggregate; missing right -/// operand (oneof unset on the wire); inner value-shape failures -/// on the literal-value branch. +/// malformation: unknown discriminant on the aggregate function or +/// operator; missing aggregate; missing right operand (oneof unset +/// on the wire); inner value-shape failures on the literal-value +/// branch. +/// +/// `HAVING` is a boolean per-group predicate and nothing else, so the +/// wire's `right` oneof has exactly one arm and this function has +/// exactly one thing to decode. Cross-group ranking is expressed with +/// SQL's own ordering surface — `ORDER BY DESC +/// LIMIT n [OFFSET m]` — which arrives as an `OrderClause` and never +/// reaches here. #[allow(dead_code)] pub(super) fn having_clause_from_proto( clause: ProtoHavingClause, @@ -328,17 +301,13 @@ pub(super) fn having_clause_from_proto( let operator = having_operator_from_proto(clause.operator)?; let right = clause.right.ok_or_else(|| { QueryError::InvalidArgument( - "HavingClause has no right operand set; every clause must carry \ - either a concrete `DocumentFieldValue` (`right.value`) or a \ - cross-group ranking reference (`right.ranking`)" + "HavingClause has no right operand set; every clause must carry a \ + concrete `DocumentFieldValue` (`right.value`)" .to_string(), ) })?; let right = match right { having_clause::Right::Value(v) => HavingRightOperand::Value(value_from_proto(v)?), - having_clause::Right::Ranking(r) => { - HavingRightOperand::Ranking(having_ranking_from_proto(r)?) - } }; Ok(HavingClause { aggregate, diff --git a/packages/rs-drive-abci/src/query/document_query/v1/mod.rs b/packages/rs-drive-abci/src/query/document_query/v1/mod.rs index 1414ced3826..def61a80a91 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/mod.rs @@ -29,7 +29,9 @@ mod compute_aggregate_mode_and_check_limit; mod conversions; -use self::compute_aggregate_mode_and_check_limit::compute_aggregate_mode_and_check_limit; +use self::compute_aggregate_mode_and_check_limit::{ + compute_aggregate_mode_and_check_limit, AggregateRouting, +}; use crate::error::query::QueryError; use crate::error::Error; @@ -41,9 +43,9 @@ use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v0::St use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::Start as RequestV1Start; use dapi_grpc::platform::v0::get_documents_request::GetDocumentsRequestV1; use dapi_grpc::platform::v0::get_documents_response::get_documents_response_v1::{ - average_results, count_results, result_data, sum_results, AverageAggregate, AverageEntries, - AverageEntry, AverageResults, CountEntries, CountEntry, CountResults, Documents, ResultData, - SumEntries, SumEntry, SumResults, + average_results, count_results, ranked_entry, result_data, sum_results, AverageAggregate, + AverageEntries, AverageEntry, AverageResults, CountEntries, CountEntry, CountResults, + Documents, RankedEntries, RankedEntry, ResultData, SumEntries, SumEntry, SumResults, }; use dapi_grpc::platform::v0::get_documents_response::{ get_documents_response_v0, get_documents_response_v1, GetDocumentsResponseV1, @@ -56,9 +58,10 @@ use dpp::version::PlatformVersion; use drive::error::query::QuerySyntaxError; use drive::query::{ AverageEntry as DriveAverageEntry, AverageMode, CountMode, DocumentAverageRequest, - DocumentAverageResponse, DocumentCountRequest, DocumentCountResponse, DocumentSumRequest, - DocumentSumResponse, OrderClause, SelectFunction, SelectProjection, SplitCountEntry, - SumEntry as DriveSumEntry, SumMode, WhereClause, + DocumentAverageResponse, DocumentCountRequest, DocumentCountResponse, DocumentRankedRequest, + DocumentRankedResponse, DocumentSumRequest, DocumentSumResponse, HavingClause, OrderClause, + RankedEntry as DriveRankedEntry, RankedEntryValue, SelectFunction, SelectProjection, + SplitCountEntry, SumEntry as DriveSumEntry, SumMode, WhereClause, }; use drive::util::grove_operations::GroveDBToUse; @@ -75,17 +78,19 @@ pub(super) fn not_yet_implemented(feature: &str) -> QueryError { ))) } -/// Validate the `select` × `group_by` × `having` combination -/// against the supported-shape table (see the message-level -/// docstring on `GetDocumentsRequestV1` in `platform.proto`). -/// Returns the routing decision so the handler knows whether to -/// dispatch to the documents-fetch path or the count path, and -/// which response shape to produce. +/// Validate the `select` × `group_by` × `order_by` × `having` +/// combination against the supported-shape table (see the +/// message-level docstring on `GetDocumentsRequestV1` in +/// `platform.proto`). Returns the routing decision so the handler +/// knows whether to dispatch to the documents-fetch path, the count +/// path or the ranked path, and which response shape to produce. +#[allow(clippy::too_many_arguments)] fn validate_and_route( select: &SelectProjection, limit: Option, - having_non_empty: bool, + having: &[HavingClause], group_by: &[String], + order_by: &[OrderClause], where_clauses: &[WhereClause], platform_version: &PlatformVersion, ) -> Result { @@ -123,7 +128,28 @@ fn validate_and_route( ))); } - if having_non_empty { + // HAVING is only ever meaningful for an aggregate projection: + // it is a boolean predicate over the groups a `COUNT` / `SUM` / + // `AVG` produces. Those three functions route through the + // versioned `compute_aggregate_mode_and_check_limit` helper below, + // which rejects non-empty HAVING on both tables (evaluation is not + // implemented) but with wording that depends on whether the + // request is otherwise a ranked one. + // + // For every other SELECT there is no aggregate for a HAVING to + // talk about, so the rejection stays here and stays unversioned. + // It also stays *ahead* of the per-function gates below, exactly + // where the old blanket rejection was: `SELECT DOCUMENTS … HAVING` + // must keep reporting the HAVING rather than silently dropping it + // on the documents path, and `SELECT MIN/MAX … HAVING` must not + // start reporting MIN/MAX as the reason a request with two + // unsupported features was refused. + if !having.is_empty() + && !matches!( + select.function, + SelectFunction::Count | SelectFunction::Sum | SelectFunction::Avg + ) + { return Err(not_yet_implemented("HAVING clause")); } @@ -184,17 +210,22 @@ fn validate_and_route( .to_string(), )); } - let mode = compute_aggregate_mode_and_check_limit( + match compute_aggregate_mode_and_check_limit( + select, group_by, where_clauses, + order_by, limit, + having, "SUM", platform_version, - )?; - Ok(RoutingDecision::Sum { - sum_property: select.field.clone(), - mode, - }) + )? { + AggregateRouting::Grouped(mode) => Ok(RoutingDecision::Sum { + sum_property: select.field.clone(), + mode, + }), + AggregateRouting::Ranked => Ok(RoutingDecision::Ranked), + } } SelectFunction::Avg => { // SELECT AVG(field): routes to @@ -223,31 +254,36 @@ fn validate_and_route( .to_string(), )); } - let mode = compute_aggregate_mode_and_check_limit( + match compute_aggregate_mode_and_check_limit( + select, group_by, where_clauses, + order_by, limit, + having, "AVG", platform_version, - )?; - Ok(RoutingDecision::Average { - sum_property: select.field.clone(), - mode, - }) + )? { + AggregateRouting::Grouped(mode) => Ok(RoutingDecision::Average { + sum_property: select.field.clone(), + mode, + }), + AggregateRouting::Ranked => Ok(RoutingDecision::Ranked), + } } SelectFunction::Min => Err(not_yet_implemented( "SELECT MIN (the wire surface accepts MIN(field) so callers \ can encode it ahead of server support landing, but the \ server doesn't yet evaluate per-group MIN; semantically \ - distinct from `HavingRanking::Min` which is a cross-group \ - ranking primitive)", + distinct from asking for the lowest-ranked group, which is \ + `ORDER BY ASC LIMIT 1`)", )), SelectFunction::Max => Err(not_yet_implemented( "SELECT MAX (the wire surface accepts MAX(field) so callers \ can encode it ahead of server support landing, but the \ server doesn't yet evaluate per-group MAX; semantically \ - distinct from `HavingRanking::Max` which is a cross-group \ - ranking primitive)", + distinct from asking for the highest-ranked group, which is \ + `ORDER BY DESC LIMIT 1`)", )), SelectFunction::Count => { if !select.field.is_empty() { @@ -278,14 +314,19 @@ fn validate_and_route( // but the `any` shape is used here too so the routing // logic doesn't bake in an assumption that could go // stale if that validator's contract ever relaxes. - let mode = compute_aggregate_mode_and_check_limit( + match compute_aggregate_mode_and_check_limit( + select, group_by, where_clauses, + order_by, limit, + having, "COUNT", platform_version, - )?; - Ok(RoutingDecision::Count(mode)) + )? { + AggregateRouting::Grouped(mode) => Ok(RoutingDecision::Count(mode)), + AggregateRouting::Ranked => Ok(RoutingDecision::Ranked), + } } } } @@ -320,6 +361,51 @@ enum RoutingDecision { sum_property: String, mode: CountMode, }, + /// Ranked routing: a `COUNT` / `SUM` / `AVG` select with a + /// `GROUP BY` whose single `ORDER BY` clause names the selected + /// aggregate. Routing does not read the direction, the limit or the + /// offset — those are drive's to resolve and to refuse. Dispatches + /// to [`Self::dispatch_ranked_v1`] → + /// `Drive::execute_document_ranked_request` and emits the + /// `RankedEntries` proto message. + /// + /// Carries nothing, unlike its `Sum` / `Average` siblings: those + /// pass the projected property forward because their drive + /// request takes it as a separate field, whereas the ranked drive + /// request takes the whole `(select, group_by, order_by, limit, + /// offset)` set and resolves the axis, direction, `k` and skip + /// itself. The handler still owns all of them after routing, so + /// there is nothing to carry and no opportunity for the routing + /// layer's reading of the ranking to drift from drive's. + Ranked, +} + +/// The `OFFSET` gate, applied **after** routing. +/// +/// Offset pagination exists on exactly one path: the ranked executor, +/// where `OFFSET m` is the rank the returned page starts at and costs +/// nothing to prove (grovedb attests the skipped region from counted +/// subtree commitments rather than walking it). Every other v1 shape — +/// documents, and the grouped count / sum / average modes — has no +/// offset primitive behind it and keeps the rejection it has always +/// had, **message for message**: those callers paginate with +/// `start_after` / `start_at`, or by narrowing the range clause. +/// +/// The message below is load-bearing and must not be reworded: clients +/// match on it, and on a protocol version whose routing table has no +/// ranked path (v13 and earlier) it is the *only* answer an offset can +/// get, exactly as it was before the ranked surface existed. +fn reject_offset_off_the_ranked_path( + offset: Option, + decision: &RoutingDecision, +) -> Result<(), QueryError> { + if offset.is_some() && !matches!(decision, RoutingDecision::Ranked) { + return Err(not_yet_implemented( + "OFFSET pagination (use cursor pagination via `start_after` / \ + `start_at` instead)", + )); + } + Ok(()) } /// Test-only: expose the routing decision for unit tests without @@ -330,15 +416,28 @@ enum RoutingDecision { /// /// Sequence (same as the real handler at /// [`Platform::query_documents_v1`]): -/// 1. `offset.is_some()` → `not_yet_implemented("OFFSET …")` -/// 2. `where_clauses_from_proto` → propagate `InvalidArgument` / +/// 1. `where_clauses_from_proto` → propagate `InvalidArgument` / /// `Unsupported` decode errors -/// 3. `order_clauses_from_proto` → propagate aggregate-target +/// 2. `order_clauses_from_proto` → propagate aggregate-target /// rejection / `InvalidArgument` decode errors -/// 4. `selects.len() > 1` → `not_yet_implemented("multi-projection …")` -/// 5. `select_from_proto` (first element, or default documents) +/// 3. `selects.len() > 1` → `not_yet_implemented("multi-projection …")` +/// 4. `select_from_proto` (first element, or default documents) +/// 5. `having_clauses_from_proto` → propagate `InvalidArgument` +/// decode errors (unknown aggregate-function / operator +/// discriminant, missing aggregate, missing / retired right +/// operand) /// 6. [`validate_and_route`] — which itself runs `limit == Some(0)` -/// → `having_non_empty` → per-function gates → mode pick. +/// → the non-aggregate HAVING gate → per-function gates → +/// routing pick (including the versioned ranked gate). +/// 7. `offset.is_some()` on a non-ranked decision → +/// `not_yet_implemented("OFFSET …")` +/// +/// The OFFSET gate is **last**, not first: whether an offset is +/// acceptable now depends on where the request routes (the ranked +/// executor paginates by offset; nothing else does), and that is not +/// known until routing has run. On a protocol version whose table has +/// no ranked path, every offset is still refused with the identical +/// message — only its position relative to the decode gates moved. /// /// Treats an unset `select` (proto-default) the same way the /// handler does — as `SelectProjection::documents()`. @@ -348,14 +447,7 @@ pub(super) fn validate_and_route_for_tests( where_clauses: &[WhereClause], platform_version: &PlatformVersion, ) -> Result<&'static str, QueryError> { - // 1. OFFSET pagination — rejected before any decoding. - if request_v1.offset.is_some() { - return Err(not_yet_implemented( - "OFFSET pagination (use cursor pagination via `start_after` / \ - `start_at` instead)", - )); - } - // 2. WHERE decoding — wire-malformed shapes (unknown operator + // 1. WHERE decoding — wire-malformed shapes (unknown operator // discriminant, nested `DocumentFieldValue.list` beyond // depth 1, …) reject as `InvalidArgument`. Runs even // though the caller passes a separate pre-decoded @@ -363,10 +455,10 @@ pub(super) fn validate_and_route_for_tests( // the depth-cap and similar decode-time contracts aren't // exercisable otherwise. conversions::where_clauses_from_proto(request_v1.where_clauses.clone())?; - // 3. ORDER BY decoding — aggregate-target reject as + // 2. ORDER BY decoding — aggregate-target reject as // `Unsupported("ORDER BY on aggregate keys …")`. - conversions::order_clauses_from_proto(request_v1.order_by.clone())?; - // 4. Multi-projection SELECT rejection. + let order_by_clauses = conversions::order_clauses_from_proto(request_v1.order_by.clone())?; + // 3. Multi-projection SELECT rejection. if request_v1.selects.len() > 1 { return Err(not_yet_implemented( "multi-projection SELECT (the wire accepts `repeated Select` so \ @@ -377,7 +469,7 @@ pub(super) fn validate_and_route_for_tests( lands)", )); } - // 5. Decode the single Select (or default to documents). + // 4. Decode the single Select (or default to documents). let select = request_v1 .selects .first() @@ -385,17 +477,23 @@ pub(super) fn validate_and_route_for_tests( .map(conversions::select_from_proto) .transpose()? .unwrap_or_else(SelectProjection::documents); + // 5. HAVING decoding — wire-malformed clauses reject as + // `InvalidArgument` before any routing decision is taken. + let having = conversions::having_clauses_from_proto(request_v1.having.clone())?; // 6. `validate_and_route` runs the inner `limit` / `having` / // per-function gates. - validate_and_route( + let decision = validate_and_route( &select, request_v1.limit, - !request_v1.having.is_empty(), + &having, &request_v1.group_by, + &order_by_clauses, where_clauses, platform_version, - ) - .map(|d| match d { + )?; + // 7. OFFSET, now that routing is known. + reject_offset_off_the_ranked_path(request_v1.offset, &decision)?; + Ok(match decision { RoutingDecision::Documents => "documents", RoutingDecision::Count(CountMode::Aggregate) => "count_aggregate", RoutingDecision::Count(CountMode::GroupByIn) => "count_entries_via_in_field", @@ -409,6 +507,10 @@ pub(super) fn validate_and_route_for_tests( // `dispatch_average_v1` further routes by where-shape × // prove flag once the executor lands. RoutingDecision::Average { .. } => "average", + // Ranked surface — single label; the axis / direction / `k` + // breakdown is drive's to resolve, not routing's, so there + // is no sub-mode to report here. + RoutingDecision::Ranked => "ranked", }) } @@ -435,17 +537,13 @@ impl Platform { offset, } = request_v1; - // OFFSET pagination is not yet implemented — cursor - // pagination via `start_after` / `start_at` is the - // supported path today. Reject any non-None offset - // before doing further work; same `not_yet_implemented` - // contract as HAVING / SUM / AVG. - if offset.is_some() { - return Ok(QueryValidationResult::new_with_error(not_yet_implemented( - "OFFSET pagination (use cursor pagination via `start_after` / \ - `start_at` instead)", - ))); - } + // NOTE: the OFFSET gate is no longer here. Whether an offset is + // acceptable depends on where the request routes — the ranked + // executor paginates by offset, nothing else does — so it runs + // once `validate_and_route` has answered, in + // `reject_offset_off_the_ranked_path`. Off the ranked path the + // rejection is byte-identical to the one this block used to + // emit. // Decode the proto-typed `repeated WhereClause` / `repeated // OrderClause` into drive's structured forms once, up @@ -453,17 +551,13 @@ impl Platform { // executor consume the typed clauses directly — no CBOR // envelope on the v1 path. // - // `having` is checked for non-empty before decoding rather - // than after: the server rejects non-empty HAVING - // wholesale today, so decoding the clauses just to - // discard them is pure overhead and the downstream - // dispatchers don't accept the decoded vec yet. When - // HAVING execution lands, the `is_empty()` short-circuit - // gives way to a full `having_clauses_from_proto` call - // that threads into the dispatchers — and at that point - // wire-malformed HAVING (bad discriminant, missing - // aggregate, …) starts surfacing as `InvalidArgument` - // automatically. + // `having` is decoded here too, unconditionally, even though + // no protocol version evaluates it yet: wire-malformed HAVING + // (bad discriminant, missing aggregate, missing right operand, + // a retired `ranking` right operand) surfaces as + // `InvalidArgument` rather than being masked by the blanket + // "not yet implemented", since malformed input is malformed + // regardless of which capability would have handled it. let where_clauses = match conversions::where_clauses_from_proto(proto_where_clauses) { Ok(c) => c, Err(e) => return Ok(QueryValidationResult::new_with_error(e)), @@ -472,7 +566,10 @@ impl Platform { Ok(c) => c, Err(e) => return Ok(QueryValidationResult::new_with_error(e)), }; - let having_non_empty = !having.is_empty(); + let having_clauses = match conversions::having_clauses_from_proto(having) { + Ok(c) => c, + Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + }; // `selects` is `repeated Select` on the wire. Empty // list → default-construct a `documents()` projection @@ -502,8 +599,9 @@ impl Platform { let routing = match validate_and_route( &select, limit, - having_non_empty, + &having_clauses, &group_by, + &order_by_clauses, &where_clauses, platform_version, ) { @@ -511,6 +609,10 @@ impl Platform { Err(e) => return Ok(QueryValidationResult::new_with_error(e)), }; + if let Err(e) = reject_offset_off_the_ranked_path(offset, &routing) { + return Ok(QueryValidationResult::new_with_error(e)); + } + match routing { RoutingDecision::Documents => self.dispatch_documents_v1( data_contract_id, @@ -561,6 +663,21 @@ impl Platform { platform_state, platform_version, ), + RoutingDecision::Ranked => self.dispatch_ranked_v1( + data_contract_id, + document_type, + select, + group_by, + having_clauses, + where_clauses, + order_by_clauses, + limit, + offset, + start, + prove, + platform_state, + platform_version, + ), } } @@ -1131,6 +1248,239 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } + + /// Dispatch a ranked request — a `COUNT` / `SUM` / `AVG` select + /// with a `GROUP BY` whose single `ORDER BY` clause names the + /// selected aggregate — to + /// [`Drive::execute_document_ranked_request`], and map the response + /// into a `GetDocumentsResponseV1` carrying a `RankedEntries` + /// payload (or a `Proof` payload when prove=true). + /// + /// Structurally parallel to [`Self::dispatch_count_v1`] — same + /// contract fetch, same `Error::Query` → typed-rejection mapping, + /// same prove vs no-prove split — with two differences worth + /// naming: + /// + /// 1. **The request is forwarded whole.** `where_clauses`, + /// `having`, `order_by`, `limit`, `offset` and `start` all go + /// down, including the ones a ranked request must leave empty, + /// because drive owns those rejections: the SDK's client-side + /// helpers call drive's validator with no abci in the path, so + /// re-checking here would create a second, driftable copy of + /// the grammar. The rejections come back as `Error::Query(...)` + /// and are surfaced to the caller as query errors, not internal + /// ones. `order_by` in particular is no longer refused here — + /// it is the ranking. + /// 2. **Proving an empty ranking is mapped, not propagated.** See + /// [`empty_ranking_proof_rejection`]. + #[allow(clippy::too_many_arguments)] + fn dispatch_ranked_v1( + &self, + data_contract_id: Vec, + document_type_name: String, + select: SelectProjection, + group_by: Vec, + having: Vec, + where_clauses: Vec, + order_clauses: Vec, + limit: Option, + offset: Option, + start: Option, + prove: bool, + platform_state: &PlatformState, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let contract_id: Identifier = + check_validation_result_with_data!(data_contract_id.try_into().map_err(|_| { + QueryError::InvalidArgument( + "id must be a valid identifier (32 bytes long)".to_string(), + ) + })); + + let (_, contract_fetch_info) = self.drive.get_contract_with_fetch_info_and_fee( + contract_id.to_buffer(), + None, + true, + None, + platform_version, + )?; + let contract_fetch_info = check_validation_result_with_data!(contract_fetch_info.ok_or( + QueryError::Query(QuerySyntaxError::DataContractNotFound( + "contract not found when querying from value with contract info", + )) + )); + let contract_ref = &contract_fetch_info.contract; + let document_type = check_validation_result_with_data!(contract_ref + .document_type_for_name(document_type_name.as_str()) + .map_err(|_| QueryError::InvalidArgument(format!( + "document type {} not found for contract {}", + document_type_name, contract_id + )))); + + let drive_request = DocumentRankedRequest { + contract: contract_ref, + document_type, + group_by: &group_by, + select, + having: &having, + order_by: &order_clauses, + where_clauses: &where_clauses, + limit, + offset, + has_start_at: start.is_some(), + prove, + }; + + let drive_response = + match self + .drive + .execute_document_ranked_request(drive_request, None, platform_version) + { + Ok(r) => r, + Err(drive::error::Error::Query(qe)) => { + return Ok(QueryValidationResult::new_with_error(QueryError::Query(qe))); + } + Err(e) => match empty_ranking_proof_rejection(&e) { + Some(rejection) => { + return Ok(QueryValidationResult::new_with_error(rejection)); + } + None => return Err(e.into()), + }, + }; + + let response = match drive_response { + DocumentRankedResponse::Entries(page) => GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Data(ResultData { + // No aggregate-collapse arm here, unlike count / + // sum / average: a ranked result is always a list + // of groups. Even `LIMIT 1` returns one *entry*, + // because the caller needs to know which group + // won, not only the winning value. + variant: Some(result_data::Variant::Ranked(RankedEntries { + // Order is preserved verbatim: entry order is + // the ranking order, and drive already + // asserted the list is no longer than `k`. + entries: page.entries.into_iter().map(into_v1_ranked_entry).collect(), + // The page's starting rank, so entry `i` is + // identifiable as the group at rank + // `skipped + i` rather than as "one of the + // top few". This is the *unproven* path, so + // the number is only as good as the node — + // which is exactly why a proving client + // ignores it and re-derives the attested + // value from the proof bytes instead (see + // `RankedPage::skipped`). Sent as `Some` + // unconditionally, including the `0` an + // offset-less query produces: the proto field + // is `optional` to keep "this node predates + // the field" distinguishable from "this page + // starts at rank 0", and collapsing 0 to + // `None` would throw that distinction away. + skipped: Some(page.skipped), + })), + })), + metadata: Some(self.response_metadata_v0(platform_state, CheckpointUsed::Current)), + }, + DocumentRankedResponse::Proof(proof_bytes) => { + let (grovedb_used, proof) = + self.response_proof_v0(platform_state, proof_bytes, GroveDBToUse::Current)?; + GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Proof(proof)), + metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), + } + } + }; + + Ok(QueryValidationResult::new_with_data(response)) + } +} + +/// Translate an rs-drive `RankedEntry` into the wire `RankedEntry`. +/// Mirror of [`into_v1_entry`] / [`into_v1_sum_entry`] / +/// [`into_v1_average_entry`] for the ranked surface. +/// +/// `key` passes through as the raw index-key bytes of the grouped +/// property value — the same bytes the proof commits to, so a client +/// verifying the proof compares byte-for-byte against what it +/// reconstructs. +/// +/// The `value` oneof is always set: drive's `RankedEntryValue` has no +/// "absent" variant (unlike the count / sum entry types, whose +/// `Option` exists for the SDK's synthesize-for-missing-In-value +/// concept — a ranked result has no caller-supplied key set to be +/// silent about). +fn into_v1_ranked_entry(e: DriveRankedEntry) -> RankedEntry { + RankedEntry { + key: e.key, + value: Some(match e.value { + RankedEntryValue::Count(count) => ranked_entry::Value::Count(count), + RankedEntryValue::Sum(sum) => ranked_entry::Value::Sum(sum), + // The wire carries a `double` approximation of the exact + // fixed-point `i128` the Avg axis is ordered by: + // `fixed_point as f64 / RANKED_AVG_SCALE as f64`, which is + // what `as_f64` computes. Lossy by construction, and that + // is fine — these entries are only read on the no-proof + // ("quick answer") path. A proof-verifying client ignores + // this field and reconstructs the exact fixed point from + // the grovedb proof, so no verification depends on it. + // Ranking order is still exact: the ordering happened over + // the i128 before this conversion. + value @ RankedEntryValue::AvgFixedPoint(_) => ranked_entry::Value::Avg(value.as_f64()), + }), + } +} + +/// Recognize the one grovedb failure that is a caller-facing +/// condition rather than a server fault: **an empty ranking cannot be +/// proved**. +/// +/// **This is now a backstop rather than a live path.** The ranked +/// prover moved to `prove_indexed_axis_top_k_paginated`, which emits a +/// guaranteed-empty range against an empty axis secondary instead of +/// refusing, so proving a ranking over a contract with no documents +/// succeeds and the proved and unproven paths agree (pinned by +/// `ranked_tests::proving_an_empty_ranking_succeeds`). The mapping is +/// kept because the failure it recognizes is a *class* — a merk-level +/// "cannot prove an empty tree" surfacing from somewhere in the +/// ancestor chain — not a single call site, and because the cost of +/// keeping it is one string comparison on an error path. +/// +/// Historically: the non-paginated prover had no absence-proof shape +/// for "this axis secondary has no entries", so proving a ranking over +/// an index that held no documents failed with a merk-level "Cannot +/// create proof for empty tree", wrapped by grovedb as +/// `CorruptedData`. Reaching that state needed nothing exotic — +/// querying a freshly registered contract with `prove = true` did it — +/// so letting it propagate would answer an ordinary request with an +/// internal error (`Status::unknown`) and an alarming server-side log +/// line, and give the caller no idea that the same request without +/// `prove` succeeded and returned the empty list. +/// +/// Detection is by variant + marker substring rather than by a typed +/// error, because grovedb flattens the merk error into a +/// `CorruptedData(String)` at the indexed-axis proof boundary; the +/// substring is the merk-side constant. The match is deliberately +/// narrow: any other `CorruptedData` still propagates as an internal +/// error, because for every other cause that classification is +/// correct. +fn empty_ranking_proof_rejection(error: &drive::error::Error) -> Option { + let drive::error::Error::GroveDB(grove_error) = error else { + return None; + }; + let drive::query::GroveError::CorruptedData(message) = grove_error.as_ref() else { + return None; + }; + if !message.contains("Cannot create proof for empty tree") { + return None; + } + Some(QueryError::InvalidArgument( + "this ranking has no groups yet, and an empty ranking cannot be proved: \ + grovedb has no absence-proof shape for an empty axis secondary. Retry \ + with `prove = false` — the unproven read answers the same request with \ + an empty entry list. Once the index holds at least one document, the \ + proved form works." + .to_string(), + )) } fn into_v1_entry(e: SplitCountEntry) -> CountEntry { diff --git a/packages/rs-drive-abci/src/query/document_query/v1/tests.rs b/packages/rs-drive-abci/src/query/document_query/v1/tests.rs index cee03f9d818..b3aeb70b843 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/tests.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/tests.rs @@ -90,10 +90,9 @@ fn oc(field: &str, ascending: bool) -> ProtoOrderClause { /// rejection tests — the server rejects any non-empty `having` /// wholesale today, so the specific aggregate function / operator /// / value here don't need to be domain-meaningful, only -/// well-formed. Tests that need the ranking right-operand -/// (`COUNT EQ MAX`, `COUNT IN TOP(5)`, …) should build the -/// `ProtoHavingClause` inline with `having_clause::Right::Ranking` -/// rather than route through this helper. +/// well-formed. A literal value is the *only* right operand the wire +/// has: `HAVING` is a boolean per-group predicate, and cross-group +/// ranking rides `ORDER BY LIMIT n` instead. fn hc( function: having_aggregate::Function, field: &str, @@ -247,11 +246,12 @@ fn reject_unknown_select_enum_value_as_invalid_argument() { /// All five modes must return `QuerySyntaxError::InvalidLimit` /// with the centralized message — not five different rejection /// reasons. -/// Non-`None` `offset` is rejected as `not_yet_implemented` before -/// any other routing happens. Pins the contract for all SELECT -/// modes (DOCUMENTS / COUNT / SUM / AVG / MIN / MAX) since the -/// rejection lives in the handler entry, not the per-function -/// gate. +/// Non-`None` `offset` is rejected as `not_yet_implemented` on +/// every path except the ranked one. Pins the contract for the +/// non-ranked SELECT modes (DOCUMENTS / COUNT / SUM / AVG / MIN / +/// MAX); the ranked exception and the verbatim message are pinned +/// in [`ranked_tests::offset_is_still_rejected_off_the_ranked_path`] +/// and [`ranked_tests::offset_pages_through_a_ranking`]. #[test] fn reject_offset_uniformly_across_select_modes() { for select_helper in [select_documents(), select_count_star()] { @@ -1922,3 +1922,1113 @@ mod ported_v0_count_tests { } } } + +mod ranked_tests { + //! End-to-end coverage of the ranked + //! (`GROUP BY p ORDER BY LIMIT n OFFSET m`) + //! surface through the real v1 handler: wire request in, + //! `ResultData.ranked` (or a `Proof`) out. + //! + //! ## Fixture + //! + //! Shares rs-drive's `restaurants` fixture by path rather than + //! copying it into this crate's `tests/supporting_files`. Both + //! choices have precedent here (`crypto-card-game` is local, + //! `family-contract` is referenced across crates from + //! `perform_events_on_first_block_of_protocol_change`), and the + //! cross-crate reference is the right one for this fixture: the + //! drive-level ranked suite asserts the *values* this contract's + //! indexes produce, and this suite asserts the *wire encoding* of + //! those same values. A second copy could drift, and the drift + //! would show up as one suite passing on a contract the other + //! doesn't test. + //! + //! Its three doctypes give one per ranking axis — two ranked + //! indexes over the same property on one doctype would be a + //! `DuplicateIndexError`: + //! + //! | doctype | index | axis | aggregated property | + //! |----------|----------------------|-------|---------------------| + //! | `review` | `byRestaurant` | Avg | `grade` | + //! | `visit` | `byRestaurantVisits` | Count | — (`COUNT(*)`) | + //! | `tip` | `byRestaurantTips` | Sum | `amount` | + + use super::*; + use crate::rpc::core::MockCoreRPCLike; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + use dpp::document::{Document, DocumentV0Setters}; + use dpp::prelude::DataContract; + use dpp::tests::json_document::json_document_to_contract; + use drive::query::{RANKED_AVG_SCALE, RANKED_COUNT_ORDER_KEY}; + use std::collections::BTreeMap; + + /// Shared with rs-drive's ranked suite — see the module docs for + /// why this is a cross-crate path and not a copy. + const RESTAURANTS_CONTRACT_PATH: &str = + "../rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json"; + + /// The one property every fixture doctype groups by. + const GROUP_PROPERTY: &str = "restaurantId"; + + /// The last protocol version whose query table (v0) has no ranked + /// path at all. Ranked routing activates at 14. + const PROTOCOL_VERSION_V13: u32 = 13; + + fn register_restaurants( + platform: &Platform, + platform_version: &PlatformVersion, + ) -> DataContract { + let contract = + json_document_to_contract(RESTAURANTS_CONTRACT_PATH, false, platform_version) + .expect("expected to parse the restaurants contract"); + store_data_contract(platform, &contract, platform_version); + contract + } + + /// Insert `(restaurant, value)` rows as documents through the + /// ordinary document-insert path, so the ranked secondaries are + /// maintained by the real write path rather than being fabricated. + /// + /// `first_seed` seeds the random-document generator, which derives + /// each document's id; two calls in one test need disjoint seed + /// ranges or the second collides on an existing id. + fn insert_docs( + platform: &Platform, + contract: &DataContract, + document_type_name: &str, + aggregated_property: &str, + first_seed: u64, + rows: &[(&str, i64)], + platform_version: &PlatformVersion, + ) { + let document_type = contract + .document_type_for_name(document_type_name) + .unwrap_or_else(|_| panic!("{document_type_name} doctype exists")); + for (i, (restaurant, value)) in rows.iter().enumerate() { + let mut document: Document = document_type + .random_document(Some(first_seed + i as u64), platform_version) + .expect("random document"); + let mut properties = BTreeMap::new(); + properties.insert( + GROUP_PROPERTY.to_string(), + Value::Text(restaurant.to_string()), + ); + properties.insert(aggregated_property.to_string(), Value::I64(*value)); + document.set_properties(properties); + store_document( + platform, + contract, + document_type, + &document, + platform_version, + ); + } + } + + fn select(function: v1_select::Function, field: &str) -> Vec { + vec![V1Select { + function: function as i32, + field: field.to_string(), + }] + } + + /// The canonical ranked request: one aggregate select, one + /// `group_by` on the ranked index's property, one `order_by` + /// naming the selected aggregate, and a `limit`. Everything else is + /// left at its "unset" wire value — which is exactly what a ranked + /// request must do. + /// + /// `order_field` is the select's own field for `SUM` / `AVG`, and + /// the `$count` sentinel for `COUNT(*)`. + #[allow(clippy::too_many_arguments)] + fn ranked_request( + contract: &DataContract, + document_type: &str, + selects: Vec, + order_field: &str, + ascending: bool, + limit: Option, + offset: Option, + prove: bool, + ) -> GetDocumentsRequestV1 { + GetDocumentsRequestV1 { + data_contract_id: contract.id().to_vec(), + document_type: document_type.to_string(), + where_clauses: Vec::new(), + order_by: vec![oc(order_field, ascending)], + limit, + start: None, + prove, + selects, + group_by: vec![GROUP_PROPERTY.to_string()], + having: Vec::new(), + offset, + } + } + + /// `ranked_request` for the common "highest first, no offset, + /// no proof" shape. + fn ranked_desc( + contract: &DataContract, + document_type: &str, + selects: Vec, + order_field: &str, + limit: u32, + ) -> GetDocumentsRequestV1 { + ranked_request( + contract, + document_type, + selects, + order_field, + false, + Some(limit), + None, + false, + ) + } + + /// Run a request through the handler and unwrap the whole ranked + /// **page** — entries plus the `skipped` rank base — asserting the + /// response landed on the `ranked` variant of `ResultData` rather + /// than on `counts` / `sums` / `averages`. + fn ranked_page( + platform: &Platform, + state: &PlatformState, + request: GetDocumentsRequestV1, + platform_version: &PlatformVersion, + ) -> RankedEntries { + let result = platform + .query_documents_v1(request, state, platform_version) + .expect("query call should not error at the transport layer"); + assert!( + result.errors.is_empty(), + "expected no validation errors, got {:?}", + result.errors + ); + match result.data { + Some(GetDocumentsResponseV1 { + result: + Some(get_documents_response_v1::Result::Data(ResultData { + variant: Some(result_data::Variant::Ranked(ranked)), + })), + metadata: Some(_), + }) => ranked, + other => panic!("expected a Ranked ResultData, got {:?}", other), + } + } + + /// [`ranked_page`] for the majority of tests, which only care about + /// the entries. + fn ranked_entries( + platform: &Platform, + state: &PlatformState, + request: GetDocumentsRequestV1, + platform_version: &PlatformVersion, + ) -> Vec { + ranked_page(platform, state, request, platform_version).entries + } + + /// The first validation error a request produces. Ranked + /// rejections must arrive this way — as a `QueryError` on the + /// validation result, which the gRPC layer turns into + /// `invalid_argument` — and never as the `Err` arm, which becomes + /// an opaque internal error. + fn ranked_error( + platform: &Platform, + state: &PlatformState, + request: GetDocumentsRequestV1, + platform_version: &PlatformVersion, + ) -> QueryError { + let result = platform + .query_documents_v1(request, state, platform_version) + .expect( + "a rejected ranked request must surface as a validation error, not as a \ + transport-level / internal error", + ); + assert!( + !result.errors.is_empty(), + "expected a validation error, got data: {:?}", + result.data + ); + result.errors.into_iter().next().expect("checked non-empty") + } + + fn group_keys(entries: &[RankedEntry]) -> Vec { + entries + .iter() + .map(|entry| String::from_utf8(entry.key.clone()).expect("fixture keys are utf-8")) + .collect() + } + + fn counts(entries: &[RankedEntry]) -> Vec { + entries + .iter() + .map(|entry| match entry.value { + Some(ranked_entry::Value::Count(count)) => count, + ref other => panic!("expected a count value, got {:?}", other), + }) + .collect() + } + + fn sums(entries: &[RankedEntry]) -> Vec { + entries + .iter() + .map(|entry| match entry.value { + Some(ranked_entry::Value::Sum(sum)) => sum, + ref other => panic!("expected a sum value, got {:?}", other), + }) + .collect() + } + + /// Pull the wire's `avg` doubles out of the entries. + /// + /// The wire carries a `double` approximation rather than the exact + /// fixed-point `i128` because these entries only exist on the + /// no-proof path — the proof path reconstructs the exact integer + /// from the grovedb proof. Expectations are therefore built by + /// running the *same* conversion the server runs (see + /// [`expected_avg`]), so the comparison is exact rather than + /// epsilon-based. + fn avgs(entries: &[RankedEntry]) -> Vec { + entries + .iter() + .map(|entry| match entry.value { + Some(ranked_entry::Value::Avg(avg)) => avg, + ref other => panic!("expected an avg value, got {:?}", other), + }) + .collect() + } + + /// grovedb's own fixed-point average, recomputed from the + /// **re-exported** scale rather than from a literal. The scale + /// changed from 10^15 to 10^19 late in grovedb's development; a + /// hardcoded expectation here would have passed against the old + /// storage and lied about the new one. + fn expected_avg_fixed_point(sum: i64, count: u64) -> i128 { + (sum as i128) + .saturating_mul(RANKED_AVG_SCALE) + .div_euclid(count as i128) + } + + /// The double the wire is expected to carry: the fixed point above, + /// divided by the scale in `f64` — the exact computation + /// `RankedEntryValue::as_f64` performs server-side. Reproducing the + /// rounding model rather than hardcoding a decimal literal is what + /// lets these tests compare with `==`. + fn expected_avg(sum: i64, count: u64) -> f64 { + (expected_avg_fixed_point(sum, count) as f64) / (RANKED_AVG_SCALE as f64) + } + + /// `SELECT COUNT(*) GROUP BY restaurantId ORDER BY $count DESC + /// LIMIT 2` — the Count axis ranks groups by how many documents + /// they hold, and `$count` is how a projection with no field of its + /// own is named on the ordering surface. + #[test] + fn count_axis_ordered_by_the_count_sentinel_returns_ranked_entries() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + insert_docs( + &platform, + &contract, + "visit", + "guests", + 1, + &[ + ("alpha", 2), + ("beta", 4), + ("beta", 2), + ("beta", 6), + ("gamma", 3), + ("gamma", 1), + ("delta", 5), + ("delta", 5), + ("delta", 5), + ("delta", 5), + ], + version, + ); + + let entries = ranked_entries( + &platform, + &state, + ranked_desc( + &contract, + "visit", + select(v1_select::Function::Count, ""), + RANKED_COUNT_ORDER_KEY, + 2, + ), + version, + ); + + assert_eq!( + group_keys(&entries), + vec!["delta", "beta"], + "descending by document count: delta(4) > beta(3) > gamma(2) > alpha(1)" + ); + assert_eq!(counts(&entries), vec![4, 3]); + } + + /// A `COUNT(*)` request that orders by a *schema* property instead + /// of the `$count` sentinel is not a ranked request — it is an + /// ordinary grouped count, and must route as one. This is the + /// discriminator the whole routing decision rests on, so it is + /// pinned from the wire rather than from the drive-side unit test. + #[test] + fn count_ordered_by_a_schema_property_is_not_ranked() { + let base = GetDocumentsRequestV1 { + group_by: vec![GROUP_PROPERTY.to_string()], + selects: select(v1_select::Function::Count, ""), + ..empty_v1_request() + }; + let where_clauses = [WhereClause { + field: GROUP_PROPERTY.to_string(), + operator: WhereOperator::GreaterThan, + value: platform_value!("a"), + }]; + + // `ORDER BY restaurantId` — a real column, not the aggregate. + let by_property = GetDocumentsRequestV1 { + order_by: vec![oc(GROUP_PROPERTY, true)], + ..base.clone() + }; + assert_eq!( + validate_and_route_for_tests(&by_property, &where_clauses, PlatformVersion::latest()) + .expect("a range-bound GROUP BY is a supported non-ranked shape"), + "count_entries_via_range_field", + "ordering by a document property leaves the request on the grouped path" + ); + + // A bare `count` column name is not the sentinel either. + let by_lookalike = GetDocumentsRequestV1 { + order_by: vec![oc("count", true)], + ..base.clone() + }; + assert_eq!( + validate_and_route_for_tests(&by_lookalike, &where_clauses, PlatformVersion::latest()) + .expect("still the grouped path"), + "count_entries_via_range_field", + ); + + // The sentinel is what flips it. + let ranked = GetDocumentsRequestV1 { + order_by: vec![oc(RANKED_COUNT_ORDER_KEY, false)], + limit: Some(5), + ..base + }; + assert_eq!( + validate_and_route_for_tests(&ranked, &where_clauses, PlatformVersion::latest()) + .expect("ORDER BY $count is the ranked shape"), + "ranked", + ); + } + + /// `SELECT SUM(amount) … ORDER BY amount DESC LIMIT 3` — the Sum + /// axis ranks by the running total of the index's summable + /// property, named on the ordering surface by that same field. + #[test] + fn sum_axis_ordered_by_the_summed_field_returns_ranked_entries() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + insert_docs( + &platform, + &contract, + "tip", + "amount", + 1, + &[ + ("alpha", 10), + ("alpha", 15), + ("beta", 100), + ("gamma", 7), + ("gamma", 8), + ("gamma", 9), + ], + version, + ); + + let entries = ranked_entries( + &platform, + &state, + ranked_desc( + &contract, + "tip", + select(v1_select::Function::Sum, "amount"), + "amount", + 3, + ), + version, + ); + + assert_eq!( + group_keys(&entries), + vec!["beta", "alpha", "gamma"], + "descending by summed amount: beta(100) > alpha(25) > gamma(24)" + ); + assert_eq!(sums(&entries), vec![100, 25, 24]); + } + + /// The headline shape: **top 5 restaurants by average grade**. + /// `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC + /// LIMIT 5`. This is the exact request the SDK will encode, so the + /// wire values are asserted field by field. + #[test] + fn avg_axis_top_k_returns_fixed_point_entries() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + insert_docs( + &platform, + &contract, + "review", + "grade", + 1, + &[ + ("alpha", 90), + ("alpha", 80), + ("beta", 60), + ("beta", 70), + ("beta", 50), + ("gamma", 95), + ("delta", 40), + ("delta", 20), + // 21/2 = 10.5 — a non-integral average, so the + // fixed-point floor is actually exercised. + ("epsilon", 10), + ("epsilon", 11), + ], + version, + ); + + let entries = ranked_entries( + &platform, + &state, + ranked_desc( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + 5, + ), + version, + ); + + assert_eq!( + group_keys(&entries), + vec!["gamma", "alpha", "beta", "delta", "epsilon"], + "descending by average grade: gamma(95) > alpha(85) > beta(60) > delta(30) \ + > epsilon(10.5)" + ); + assert_eq!( + avgs(&entries), + vec![ + expected_avg(95, 1), + expected_avg(170, 2), + expected_avg(180, 3), + expected_avg(60, 2), + expected_avg(21, 2), + ], + "each entry is floor(sum * RANKED_AVG_SCALE / count) rendered as a double" + ); + // And the double really is the average a caller would render — + // 21/2 survives the fixed-point round trip exactly. + assert_eq!(avgs(&entries)[4], 10.5); + } + + /// **`OFFSET` is accepted on the ranked path**, which is the one + /// place in the v1 surface where it is. `LIMIT 1 OFFSET 4` is how + /// "the 5th best average grade" is asked for, and a window running + /// past the end comes back short rather than erroring. + #[test] + fn offset_pages_through_a_ranking() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + insert_docs( + &platform, + &contract, + "review", + "grade", + 1, + &[ + ("gamma", 95), + ("alpha", 85), + ("beta", 60), + ("delta", 30), + ("epsilon", 10), + ], + version, + ); + + let paged = |limit, offset| { + ranked_request( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + false, + Some(limit), + Some(offset), + false, + ) + }; + + // The 5th best grade. + let fifth = ranked_page(&platform, &state, paged(1, 4), version); + assert_eq!( + group_keys(&fifth.entries), + vec!["epsilon"], + "gamma > alpha > beta > delta > epsilon — rank 4 (0-based) is epsilon" + ); + assert_eq!(avgs(&fifth.entries), vec![expected_avg(10, 1)]); + assert_eq!( + fifth.skipped, + Some(4), + "the page echoes the rank it starts at, which is what makes the single \ + entry identifiable as the *5th* best rather than the best" + ); + + // Rank 0 still reports a base, so a caller never has to guess + // whether an absent `skipped` means "rank 0" or "old node". + let best = ranked_page(&platform, &state, paged(1, 0), version); + assert_eq!(group_keys(&best.entries), vec!["gamma"]); + assert_eq!(best.skipped, Some(0)); + + // A window that spans the end returns the tail, short. + let tail = ranked_page(&platform, &state, paged(3, 3), version); + assert_eq!( + group_keys(&tail.entries), + vec!["delta", "epsilon"], + "only two groups remain from rank 3" + ); + assert_eq!(tail.skipped, Some(3)); + + // A window entirely past the end is an empty page, not an + // error. On this *unproven* path grovedb's read API doesn't + // report the short walk, so `skipped` echoes the request; the + // proved path is where it becomes the attested population. + let past_end = ranked_page(&platform, &state, paged(2, 9), version); + assert!( + past_end.entries.is_empty(), + "there is no rank 9 in a five-group ranking, and asking for one is not an \ + error — got {:?}", + group_keys(&past_end.entries) + ); + assert_eq!(past_end.skipped, Some(9)); + + // And the same page proves. + let result = platform + .query_documents_v1( + ranked_request( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + false, + Some(1), + Some(4), + true, + ), + &state, + version, + ) + .expect("query should succeed"); + assert!(result.errors.is_empty(), "got {:?}", result.errors); + match result.data { + Some(GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Proof(proof)), + .. + }) => assert!(!proof.grovedb_proof.is_empty()), + other => panic!("expected a Proof response, got {:?}", other), + } + } + + /// **OFFSET stays refused everywhere else.** The relaxation is + /// scoped to the ranked path and to nothing else: documents + /// fetches and the grouped count / sum / average modes keep the + /// exact `not_yet_implemented` message they have always returned, + /// character for character, because clients match on it. + #[test] + fn offset_is_still_rejected_off_the_ranked_path() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + + const EXPECTED: &str = "OFFSET pagination (use cursor pagination via \ + `start_after` / `start_at` instead) is not yet implemented"; + + // SELECT DOCUMENTS with an offset. + let documents = GetDocumentsRequestV1 { + data_contract_id: contract.id().to_vec(), + document_type: "review".to_string(), + offset: Some(3), + ..empty_v1_request() + }; + match ranked_error(&platform, &state, documents, version) { + QueryError::Query(QuerySyntaxError::Unsupported(message)) => { + assert_eq!( + message, EXPECTED, + "the documents-path message must not drift" + ); + } + other => panic!("expected the OFFSET rejection, got {other:?}"), + } + + // SELECT AVG with a GROUP BY but ordered by a document + // property — a grouped aggregate, not a ranking. The range + // where clause is what makes this an otherwise *routable* + // grouped request, so the OFFSET gate is the first thing it + // trips rather than the second. + let grouped = GetDocumentsRequestV1 { + data_contract_id: contract.id().to_vec(), + document_type: "review".to_string(), + selects: select(v1_select::Function::Avg, "grade"), + group_by: vec![GROUP_PROPERTY.to_string()], + where_clauses: vec![wc( + GROUP_PROPERTY, + ProtoWhereOperator::GreaterThan, + Value::Text("a".to_string()), + )], + order_by: vec![oc(GROUP_PROPERTY, true)], + offset: Some(3), + ..empty_v1_request() + }; + match ranked_error(&platform, &state, grouped, version) { + QueryError::Query(QuerySyntaxError::Unsupported(message)) => { + assert_eq!(message, EXPECTED, "the grouped-path message must not drift"); + } + other => panic!("expected the OFFSET rejection, got {other:?}"), + } + } + + /// `prove = true` takes the proof arm of the response envelope, + /// exactly like the count / sum / average dispatchers — the + /// ranked entries live inside the grovedb proof, not alongside it. + #[test] + fn ranked_request_with_prove_returns_a_proof() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + insert_docs( + &platform, + &contract, + "review", + "grade", + 1, + &[("alpha", 90), ("beta", 60), ("gamma", 95)], + version, + ); + + let result = platform + .query_documents_v1( + ranked_request( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + false, + Some(5), + None, + true, + ), + &state, + version, + ) + .expect("query should succeed"); + assert!( + result.errors.is_empty(), + "expected no validation errors, got {:?}", + result.errors + ); + match result.data { + Some(GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Proof(proof)), + metadata: Some(_), + }) => { + assert!( + !proof.grovedb_proof.is_empty(), + "expected non-empty grovedb proof bytes for a non-empty ranking" + ); + } + other => panic!("expected a Proof response, got {:?}", other), + } + } + + /// `ASC` walks the axis from the smallest aggregate up, and + /// `LIMIT 1` is how the single worst- (or, for `DESC`, best-) + /// ranked group is asked for. Both still return *entries*, because + /// "which group" is as much of the answer as "what value". + #[test] + fn ascending_order_returns_the_worst_ranked_groups() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + insert_docs( + &platform, + &contract, + "tip", + "amount", + 1, + &[("alpha", 10), ("beta", 100), ("gamma", 55)], + version, + ); + + let sum_request = |ascending, limit| { + ranked_request( + &contract, + "tip", + select(v1_select::Function::Sum, "amount"), + "amount", + ascending, + Some(limit), + None, + false, + ) + }; + + let bottom_two = ranked_entries(&platform, &state, sum_request(true, 2), version); + assert_eq!( + group_keys(&bottom_two), + vec!["alpha", "gamma"], + "ASC walks upward from the smallest sum" + ); + assert_eq!(sums(&bottom_two), vec![10, 55]); + + let top_one = ranked_entries(&platform, &state, sum_request(false, 1), version); + assert_eq!( + group_keys(&top_one), + vec!["beta"], + "DESC LIMIT 1 is the single largest sum" + ); + assert_eq!(sums(&top_one), vec![100]); + + let bottom_one = ranked_entries(&platform, &state, sum_request(true, 1), version); + assert_eq!( + group_keys(&bottom_one), + vec!["alpha"], + "ASC LIMIT 1 is the single smallest sum" + ); + assert_eq!(sums(&bottom_one), vec![10]); + } + + /// A boolean `HAVING` alongside an aggregate ordering is refused + /// with the `not_yet_implemented` contract — a client can leave the + /// request in place and it starts working when the capability + /// lands. + #[test] + fn having_alongside_an_aggregate_ordering_is_still_unsupported() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + + let mut request = ranked_desc( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + 3, + ); + request.having = vec![hc( + having_aggregate::Function::Avg, + "grade", + having_clause::Operator::GreaterThan, + Value::U64(4), + )]; + + match ranked_error(&platform, &state, request, version) { + QueryError::Query(QuerySyntaxError::Unsupported(message)) => { + assert!( + message.contains("not yet implemented") && message.contains("HAVING"), + "expected the HAVING-with-ordering rejection, got: {message}" + ); + } + other => panic!("expected Unsupported, got {other:?}"), + } + } + + /// Ordering by the selected aggregate **without** a `GROUP BY` is + /// not a ranking — there is one implicit group and nothing to rank + /// it against — so the request stays on the plain aggregate path + /// and is refused there for the reason that path refuses it. + #[test] + fn ordering_by_the_aggregate_without_group_by_is_not_ranked() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + + let mut request = ranked_desc( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + 3, + ); + request.group_by = Vec::new(); + + let routing_error = validate_and_route_for_tests(&request, &[], version) + .expect_err("an aggregate with a limit and no GROUP BY is refused") + .to_string(); + assert!( + routing_error.contains("limit"), + "the plain-aggregate path owns this rejection, not the ranked one; got: \ + {routing_error}" + ); + + match ranked_error(&platform, &state, request, version) { + QueryError::Query(QuerySyntaxError::InvalidLimit(message)) => { + assert!( + message.contains("empty GROUP BY"), + "expected the aggregate-path limit rejection, got: {message}" + ); + } + other => panic!("expected InvalidLimit, got {other:?}"), + } + } + + /// `limit` is **required** on the ranked path — it is the `k` the + /// proof envelope echoes, so there is no server default a verifying + /// client could reproduce. What this pins is that drive's + /// `Error::Query` reaches the caller as a query error on the + /// validation result, rather than being swallowed into an internal + /// error by the dispatcher's `Err(e) => Err(e.into())` arm. + #[test] + fn a_ranked_request_without_a_limit_surfaces_as_a_query_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + + let request = ranked_request( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + false, + None, + None, + false, + ); + + match ranked_error(&platform, &state, request, version) { + QueryError::Query(QuerySyntaxError::InvalidLimit(message)) => { + assert!( + message.contains("require an explicit `limit`"), + "expected drive's own limit rejection to reach the caller verbatim, \ + got: {message}" + ); + } + other => panic!("expected InvalidLimit from drive, got {other:?}"), + } + } + + /// A limit over the ranked ceiling is drive's rejection too, and + /// reaches the caller the same way. + #[test] + fn a_ranked_limit_over_the_ceiling_surfaces_as_a_query_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + + let request = ranked_desc( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + 101, + ); + + match ranked_error(&platform, &state, request, version) { + QueryError::Query(QuerySyntaxError::InvalidLimit(message)) => { + assert!( + message.contains("ceiling of 100"), + "expected drive's ceiling rejection, got: {message}" + ); + } + other => panic!("expected InvalidLimit from drive, got {other:?}"), + } + } + + /// **Empty rankings prove.** grovedb's paginated prover emits a + /// guaranteed-empty range for an empty axis secondary rather than + /// refusing, so a freshly registered contract queried with + /// `prove = true` gets a proof instead of the "cannot prove an + /// empty tree" error the non-paginated prover used to raise. The + /// unproven read of the same request returns an empty list, so the + /// two paths agree. + #[test] + fn proving_an_empty_ranking_succeeds() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = register_restaurants(&platform, version); + + let request = |prove| { + ranked_request( + &contract, + "review", + select(v1_select::Function::Avg, "grade"), + "grade", + false, + Some(5), + None, + prove, + ) + }; + + let entries = ranked_entries(&platform, &state, request(false), version); + assert!( + entries.is_empty(), + "an index with no documents has no groups to rank" + ); + + let result = platform + .query_documents_v1(request(true), &state, version) + .expect("query should succeed"); + assert!( + result.errors.is_empty(), + "proving an empty ranking must not be a rejection any more, got {:?}", + result.errors + ); + match result.data { + Some(GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Proof(proof)), + .. + }) => assert!(!proof.grovedb_proof.is_empty()), + other => panic!("expected a Proof response, got {:?}", other), + } + + // One document, and the identical request keeps proving. + insert_docs( + &platform, + &contract, + "review", + "grade", + 1, + &[("alpha", 42)], + version, + ); + let result = platform + .query_documents_v1(request(true), &state, version) + .expect("query should succeed"); + assert!(result.errors.is_empty(), "got {:?}", result.errors); + } + + /// **The activation gate.** Protocol version 13's query table + /// selects the v0 routing helper, which has no ranked path: it + /// reads the request as an ordinary grouped aggregate and refuses + /// it because the `GROUP BY` property carries no `In` or range + /// where clause. A v13 node and a v14 node must disagree here and + /// nowhere else, which is what lets a mixed-version network run + /// through the upgrade. + /// + /// The contract is never fetched (the gate fires first), so this + /// deliberately runs against a bare platform: registering the + /// restaurants fixture under v13 would fail anyway, since + /// meta-schema v2 rejects the `ranked*` keywords. + #[test] + fn protocol_version_13_does_not_route_ranked() { + let (platform, state, version) = + setup_platform(None, Network::Testnet, Some(PROTOCOL_VERSION_V13)); + assert_eq!( + version.protocol_version, PROTOCOL_VERSION_V13, + "this test is only meaningful on the v13 table" + ); + + let request = GetDocumentsRequestV1 { + data_contract_id: vec![0u8; 32], + document_type: "review".to_string(), + where_clauses: Vec::new(), + order_by: vec![oc("grade", false)], + limit: Some(5), + start: None, + prove: false, + selects: select(v1_select::Function::Avg, "grade"), + group_by: vec![GROUP_PROPERTY.to_string()], + having: Vec::new(), + offset: None, + }; + + match ranked_error(&platform, &state, request.clone(), version) { + QueryError::Query(QuerySyntaxError::Unsupported(message)) => { + assert!( + message.contains("GROUP BY on field 'restaurantId'") + && message.contains("not yet implemented"), + "v13 must read a ranked-shaped request as an unsupported grouped \ + aggregate, got: {message}" + ); + } + other => panic!("expected the v13 grouped rejection, got {other:?}"), + } + + // And an OFFSET on v13 is refused exactly as it always was — + // the relaxation is gated behind the ranked route, which v13 + // never takes. + let with_offset = GetDocumentsRequestV1 { + offset: Some(2), + ..request + }; + match ranked_error(&platform, &state, with_offset, version) { + QueryError::Query(QuerySyntaxError::Unsupported(message)) => { + assert!( + message.contains("GROUP BY on field 'restaurantId'"), + "routing still runs first on v13; the offset gate is downstream of \ + it, got: {message}" + ); + } + other => panic!("expected the v13 grouped rejection, got {other:?}"), + } + } + + /// The routing decision itself, without a platform: an `ORDER BY` + /// naming the selected aggregate routes to "ranked" on each + /// aggregate select, and the same request ordered by a document + /// property routes exactly where it did before. Guards the half of + /// the v1 helper that is supposed to be a no-op. + #[test] + fn routing_picks_ranked_only_when_order_by_names_the_selected_aggregate() { + let base = GetDocumentsRequestV1 { + group_by: vec![GROUP_PROPERTY.to_string()], + ..empty_v1_request() + }; + let where_clauses = [WhereClause { + field: GROUP_PROPERTY.to_string(), + operator: WhereOperator::GreaterThan, + value: platform_value!("a"), + }]; + + for (selects, order_field) in [ + ( + select(v1_select::Function::Count, ""), + RANKED_COUNT_ORDER_KEY, + ), + (select(v1_select::Function::Sum, "amount"), "amount"), + (select(v1_select::Function::Avg, "grade"), "grade"), + ] { + let ranked = GetDocumentsRequestV1 { + selects: selects.clone(), + order_by: vec![oc(order_field, false)], + limit: Some(5), + ..base.clone() + }; + assert_eq!( + validate_and_route_for_tests(&ranked, &[], PlatformVersion::latest()).unwrap(), + "ranked", + "an ORDER BY on the selected aggregate routes to the ranked executor" + ); + + // Same select, ordered by a document property: unchanged + // pre-ranked routing, driven by the where-clause shape as + // before. + let grouped = GetDocumentsRequestV1 { + selects, + order_by: vec![oc(GROUP_PROPERTY, true)], + ..base.clone() + }; + let label = + validate_and_route_for_tests(&grouped, &where_clauses, PlatformVersion::latest()) + .expect("a range-bound GROUP BY is a supported non-ranked shape"); + assert_ne!( + label, "ranked", + "ordering by a document property means no ranked routing" + ); + + // And no ORDER BY at all is likewise not ranked. + let unordered = GetDocumentsRequestV1 { + order_by: Vec::new(), + ..grouped + }; + let label = + validate_and_route_for_tests(&unordered, &where_clauses, PlatformVersion::latest()) + .expect("a range-bound GROUP BY is a supported non-ranked shape"); + assert_ne!(label, "ranked", "no ORDER BY means no ranked routing"); + } + } +} diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs index 91932028afc..8b0e1332473 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs @@ -25,6 +25,7 @@ mod tests { use drive_abci::platform_types::platform_state::PlatformStateV0Methods; use drive_abci::test::helpers::setup::TestPlatformBuilder; use platform_version::version::v1::PROTOCOL_VERSION_1; + use platform_version::version::v13::PROTOCOL_VERSION_13; use platform_version::version::PlatformVersion; use rand::prelude::StdRng; use rand::SeedableRng; @@ -189,6 +190,94 @@ mod tests { assert_eq!(balance, 99864009940) } + #[tokio::test] + async fn run_chain_one_identity_in_solitude_protocol_version_13() { + // Pins the fee shape at protocol version 13. The grove v4 cleanup + // gates active from v14 derive their inspection from data the merk + // apply already loads, so they are cost-neutral: this balance is + // identical to the latest-version test's, and the pair proves the + // v13 -> v14 boundary changes nothing about this run's fees. + // This is different because in the root tree we added GroupActions + // DataContract_Documents 64 + // / \ + // Identities 32 Balances 96 + // / \ / \ + // Token_Balances 16 Pools 48 WithdrawalTransactions 80 Votes 112 + // / \ / \ / \ / \ + // NUPKH->I 8 UPKH->I 24 PreFundedSpecializedBalances 40 Masternode Lists 56 (reserved) SpentAssetLockTransactions 72 GroupActions 88 Misc 104 Versions 120 + + // This will cause the costs of insertion of a spent asset lock transition, since group actions now exist we will see a slight difference in processing costs + // This is because WithdrawalTransactions will have a right element in the tree. + + let platform_version = + PlatformVersion::get(PROTOCOL_VERSION_13).expect("expected to get platform version 13"); + let strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![], + start_identities: StartIdentities::default(), + start_addresses: StartAddresses::default(), + identity_inserts: IdentityInsertInfo { + frequency: Frequency { + times_per_block_range: 1..2, + chance_per_block: None, + }, + ..Default::default() + }, + + identity_contract_nonce_gaps: None, + signer: None, + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + let config = PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: 3000, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .with_initial_protocol_version(PROTOCOL_VERSION_13) + .build_with_mock_rpc(); + + let outcome = + run_chain_for_strategy(&mut platform, 2, strategy, config, 15, &mut None, &mut None) + .await; + + let balance = outcome + .abci_app + .platform + .drive + .fetch_identity_balance( + outcome.identities.first().unwrap().id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch balances") + .expect("expected to have an identity to get balance from"); + + assert_eq!(balance, 99864009940) + } + #[tokio::test] async fn run_chain_insert_one_new_identity_per_block_with_block_signing() { drive_abci::logging::init_for_tests(LogLevel::Silent); diff --git a/packages/rs-drive-proof-verifier/src/lib.rs b/packages/rs-drive-proof-verifier/src/lib.rs index 4d1accc87d8..0a5bf872d42 100644 --- a/packages/rs-drive-proof-verifier/src/lib.rs +++ b/packages/rs-drive-proof-verifier/src/lib.rs @@ -14,12 +14,40 @@ pub use proof::document_count::{ verify_distinct_count_proof, verify_point_lookup_count_proof, verify_primary_key_count_tree_proof, DocumentCount, }; +/// Verified ranked (`GROUP BY … ORDER BY LIMIT n +/// [OFFSET m]`) result types. `DocumentRankedEntries` carries one entry +/// per returned group **in ranking order**, plus the `starting_rank` +/// that pins each entry to an absolute position; +/// [`verify_ranked_top_k_proof`] is the tenderdash-composition wrapper +/// that binds the proof's reconstructed root hash to the signed app +/// hash and returns the whole verified [`drive::query::RankedPage`]. +pub use proof::document_ranked::{verify_ranked_top_k_proof, DocumentRankedEntries}; pub use proof::document_split_count::DocumentSplitCounts; // Re-export `SplitCountEntry` from rs-drive at the proof-verifier // crate root so SDK consumers don't have to depend on rs-drive // directly just to name the entry type returned by // `verify_distinct_count_proof` and `DocumentSplitCounts::from_verified`. pub use drive::query::SplitCountEntry; +// Same treatment for the ranked surface's entry types, plus the +// fixed-point scale the Avg axis sorts by. `RANKED_AVG_SCALE` is +// itself a re-export of grovedb's `AVG_FIXED_POINT_SCALE` — it moved +// from 10^15 to 10^19 late in grovedb's development, so clients must +// read it from here and never hardcode the literal. Divide an +// `AvgFixedPoint` value by it (or call `RankedEntryValue::as_f64`) to +// render an average. +// +// The fixed point is the exact integer grovedb ranks on **when it came +// from a proof**. `DocumentRankedEntries::from_unproved_response` +// reconstructs it from the wire's `double` (the no-proof path carries +// an f64 approximation, since a proof-verifying client rebuilds the +// entry from the proof instead), so on that path the low digits past +// f64's ~15–16 significant decimals are noise. Anything that needs the +// committed integer must go through the proof. +// `RankedPage` rides along because it is what +// `verify_ranked_top_k_proof` returns: a caller verifying a ranked +// proof for themselves needs to name the type without depending on +// rs-drive. +pub use drive::query::{RankedEntry, RankedEntryValue, RankedPage, RANKED_AVG_SCALE}; /// Verified average result types. Average-side analog of `DocumentSum` /// / `DocumentSplitSums`; carry the `(count, sum)` pair the verifier /// recovers from grovedb PR 670's `AggregateCountAndSumOnRange` diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 4faf625a744..eb47bdab25c 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -4,6 +4,11 @@ /// `AggregateCountAndSumOnRange` primitive. pub mod document_average; pub mod document_count; +/// Verified ranked (`GROUP BY … ORDER BY LIMIT n +/// [OFFSET m]`) result. One entry per returned group, in ranking order, +/// plus the attested rank the page starts at, read from an indexed +/// tree's per-axis secondary (grovedb PR 657); see the file's docs. +pub mod document_ranked; /// Per-entry verified average result. One `(in_key, key, count, sum)` /// tuple per matched group; client divides per-entry to obtain /// per-group averages. diff --git a/packages/rs-drive-proof-verifier/src/proof/document_ranked.rs b/packages/rs-drive-proof-verifier/src/proof/document_ranked.rs new file mode 100644 index 00000000000..6239eca48e9 --- /dev/null +++ b/packages/rs-drive-proof-verifier/src/proof/document_ranked.rs @@ -0,0 +1,737 @@ +//! Verified **ranked** (`GROUP BY … ORDER BY LIMIT n`) +//! document results. +//! +//! A ranked query answers "which `n` groups score highest (or lowest) +//! on an aggregate?" — `SELECT AVG(grade) GROUP BY restaurantId +//! ORDER BY grade DESC LIMIT 5`. The answer is read straight out of +//! the per-axis *secondary* Merk of an indexed tree (grovedb PR #657), +//! so it costs `O(log n + k)` and comes with a proof that commits to +//! exactly the `k` returned `(aggregate, group key)` pairs — plus the +//! `OFFSET`, which grovedb attests from counted subtree commitments +//! rather than by walking the skipped region, so deep pages cost the +//! same as the first one. +//! +//! This module holds the client-facing result type +//! ([`DocumentRankedEntries`]), the tenderdash-composition wrapper +//! around rs-drive's merk-level verifier +//! ([`verify_ranked_top_k_proof`]), and the decoder for the unproven +//! `ResultData.ranked` wire payload +//! ([`DocumentRankedEntries::from_unproved_response`]). +//! +//! Per-shape routing (which index covers the axis, which +//! `(axis, descending, k, offset)` tuple the request resolves to) lives +//! in rs-sdk's `ranked_proof_helpers`, exactly as count's four-way +//! dispatch lives in `count_proof_helpers` — it needs the data +//! contract, which this crate does not carry. + +use crate::error::MapGroveDbError; +use crate::verify::verify_tenderdash_proof; +use crate::{ContextProvider, Error, FromProof}; +use dapi_grpc::platform::v0::get_documents_response::get_documents_response_v1::{ + ranked_entry, result_data, RankedEntry as ProtoRankedEntry, ResultData, +}; +use dapi_grpc::platform::v0::get_documents_response::{ + get_documents_response_v1, Version as ResponseVersion, +}; +use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; +use dpp::dashcore::Network; +use dpp::version::PlatformVersion; +use drive::query::{ + DriveDocumentQuery, DriveDocumentRankedQuery, RankedEntry, RankedEntryValue, RankedPage, + RANKED_AVG_SCALE, +}; +use drive::verify::RootHash; + +/// One page of a `GROUP BY … ORDER BY LIMIT n [OFFSET m]` +/// query: the ranked groups, plus the rank the page starts at. +/// +/// **Entry order is the ranking order** — best-first for `DESC`, +/// worst-first for `ASC`. Callers must not re-sort; ties (groups with +/// equal aggregates) come back in group-key order *in the direction of +/// the walk*, which is descending group-key order for `DESC`. +/// +/// Fewer than `n` entries is normal — the index simply holds fewer +/// groups than were asked for — and is not an error. +/// +/// Each [`RankedEntry`]'s `key` is the raw index-key bytes of the +/// `GROUP BY` property's value (for a `string` property, its UTF-8 +/// bytes); its `value` is the aggregate, one of +/// [`RankedEntryValue::Count`] / [`RankedEntryValue::Sum`] / +/// [`RankedEntryValue::AvgFixedPoint`]. Averages are fixed-point +/// integers scaled by [`crate::RANKED_AVG_SCALE`]; divide by it (or +/// call [`RankedEntryValue::as_f64`]) to render one. +/// +/// The fixed point is **exact on the proved path only**. A page built +/// by [`Self::from_verified`] carries the very integer the proof +/// commits to; one built by [`Self::from_unproved_response`] carries a +/// best-effort reconstruction from the wire's `double` — see that +/// method for what that costs. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DocumentRankedEntries { + /// The 0-based rank of `entries[0]` — the query's `OFFSET`, as + /// actually honoured. + /// + /// This is what turns a page back into a *ranking*: entry `i` is + /// the group at rank `starting_rank + i`. Without it a caller who + /// asked for `ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` receives + /// one entry and has no way to tell it really is the 5th-best + /// group rather than the best. + /// + /// On the **proved** path this is grovedb's cryptographically + /// attested count, re-derived by the verifier from the counted + /// subtree commitments in the proof bytes rather than trusted from + /// the response. It equals the requested offset unless the walk ran + /// out of groups first, in which case `entries` is empty and this + /// is a *proof* that the ranking holds exactly this many groups in + /// total — an offset past the end is a positive answer, not an + /// error. + /// + /// On the **unproven** decode it is whatever the node put on the + /// wire (`0` when the field is absent), and carries no more weight + /// than the entries beside it. + pub starting_rank: u64, + /// The groups on this page, **in ranking order**. + pub entries: Vec, +} + +impl DocumentRankedEntries { + /// Build a [`DocumentRankedEntries`] from a verifier-side + /// [`RankedPage`] — the shape rs-drive's merk-level verifier + /// returns, carrying the attested skip alongside the entries. + /// + /// Mirrors + /// [`DocumentSplitCounts::from_verified`](crate::DocumentSplitCounts::from_verified), + /// except that it is not the identity: `RankedPage` is rs-drive's + /// internal type and this is the client-facing one, so the rename + /// of `skipped` → `starting_rank` happens here, where the value + /// stops being "how far the walk skipped" and starts being "which + /// rank you are looking at". + pub fn from_verified(page: RankedPage) -> Self { + DocumentRankedEntries { + starting_rank: page.skipped, + entries: page.entries, + } + } + + /// Decode the **unproven** ranked payload of a `getDocuments` + /// response — the `ResultData.ranked` variant a node returns for a + /// ranked request sent with `prove = false`. + /// + /// Order is preserved verbatim: the server emits entries in + /// ranking order and this decoder never re-sorts. + /// + /// This is a plain wire decode with **no cryptographic guarantee + /// whatsoever** — it is the "trust the node" path, and that applies + /// to [`Self::starting_rank`] every bit as much as to the entries: + /// an unproven page claiming to start at rank 4 is a claim, not a + /// fact. Prefer [`verify_ranked_top_k_proof`] (via rs-sdk's + /// `DocumentRankedEntries::fetch`) unless you are deliberately + /// reading from a node you already trust. + /// + /// A node that predates the wire `skipped` field leaves it unset; + /// that decodes to `starting_rank == 0`, which is the right answer + /// for the offset-less queries such a node could serve at all. + /// + /// ## Averages come back approximate here + /// + /// The wire's `avg` is a `double`, deliberately: these entries only + /// exist on this path, and a proof-verifying client reconstructs + /// the exact fixed point from the proof instead. To keep one + /// [`RankedEntryValue`] type across both paths this decoder + /// multiplies the double back up by [`crate::RANKED_AVG_SCALE`] and + /// rounds, so the [`RankedEntryValue::AvgFixedPoint`] it yields is a + /// **best-effort reconstruction, not the committed integer** — its + /// low digits are noise beyond `f64`'s ~15–16 significant decimal + /// digits. Render it, compare it loosely, but do not treat it as the + /// value grovedb ranked on; ask for the proof if you need that. + /// + /// # Errors + /// + /// - [`Error::EmptyVersion`] when the response carries no version. + /// - [`Error::ResponseDecodeError`] when the response is a V0 + /// response (which has no ranked shape), carries a proof rather + /// than data, or carries a non-ranked `ResultData` variant. + /// - [`Error::ResponseDecodeError`] when an entry's `value` oneof + /// is unset, or its `avg` is not a finite double that scales into + /// `i128` range. + pub fn from_unproved_response( + response: &GetDocumentsResponse, + ) -> Result<(Self, ResponseMetadata), Error> { + let version = response.version.as_ref().ok_or(Error::EmptyVersion)?; + let ResponseVersion::V1(v1) = version else { + return Err(Error::ResponseDecodeError { + error: "ranked results are a V1-only response shape; got a V0 getDocuments \ + response. Ranked queries require protocol version 14+." + .to_string(), + }); + }; + let metadata = v1.metadata.clone().ok_or(Error::EmptyResponseMetadata)?; + let (starting_rank, entries) = match v1.result.as_ref() { + Some(get_documents_response_v1::Result::Data(ResultData { + variant: Some(result_data::Variant::Ranked(ranked)), + })) => ( + ranked.skipped.unwrap_or(0), + ranked + .entries + .iter() + .map(ranked_entry_from_proto) + .collect::, _>>()?, + ), + Some(get_documents_response_v1::Result::Proof(_)) => { + return Err(Error::ResponseDecodeError { + error: "the response carries a proof, not unproven ranked entries; verify \ + it with `verify_ranked_top_k_proof` instead of decoding it" + .to_string(), + }); + } + other => { + return Err(Error::ResponseDecodeError { + error: format!( + "expected a `ResultData.ranked` payload for a ranked request, got \ + {other:?}. A response on another variant means the node routed the \ + request to a different executor — check that the request carries a \ + `group_by` and a single `order_by` naming the single `select`'s \ + aggregate (`$count` for `COUNT(*)`)." + ), + }); + } + }; + Ok(( + DocumentRankedEntries { + starting_rank, + entries, + }, + metadata, + )) + } +} + +/// Decode one wire [`ProtoRankedEntry`] into rs-drive's +/// [`RankedEntry`]. +/// +/// The `avg` arm re-scales the wire's `double` into the fixed-point +/// `i128` [`RankedEntryValue`] carries, so callers see one type +/// regardless of which path produced the page. The round trip is lossy +/// in one direction only — the server divided an exact integer by +/// [`RANKED_AVG_SCALE`], we multiply back and round — so the result is +/// the closest fixed point to what the node reported, not necessarily +/// the one it committed to. `from_unproved_response` documents that; +/// exactness lives on the proof path. +/// +/// A non-finite `avg`, or one that scales past `i128`, is rejected +/// rather than saturated: `as` casts would silently turn `NaN` into +/// `0` (an average of zero — a plausible-looking lie) and an +/// out-of-range double into `i128::MIN`/`MAX`. Every legitimate value +/// fits comfortably, since `|sum| ≤ i64::MAX` bounds the true fixed +/// point at `i64::MAX * 10^19 ≈ 9.2e37 < i128::MAX`. +fn ranked_entry_from_proto(entry: &ProtoRankedEntry) -> Result { + let value = match entry.value.as_ref() { + Some(ranked_entry::Value::Count(count)) => RankedEntryValue::Count(*count), + Some(ranked_entry::Value::Sum(sum)) => RankedEntryValue::Sum(*sum), + Some(ranked_entry::Value::Avg(avg)) => { + let scaled = avg * (RANKED_AVG_SCALE as f64); + // `i128::MIN as f64` is exactly −2^127; `i128::MAX as f64` + // rounds *up* to 2^127, hence the asymmetric comparisons. + if !scaled.is_finite() || scaled < (i128::MIN as f64) || scaled >= -(i128::MIN as f64) { + return Err(Error::ResponseDecodeError { + error: format!( + "`avg` must be a finite double that scales into i128 range when \ + multiplied by {RANKED_AVG_SCALE}, got {avg}" + ), + }); + } + RankedEntryValue::AvgFixedPoint(scaled.round() as i128) + } + None => { + return Err(Error::ResponseDecodeError { + error: "ranked entry carries no `value`; the server always sets exactly one \ + of `count` / `sum` / `avg`" + .to_string(), + }); + } + }; + Ok(RankedEntry { + key: entry.key.clone(), + value, + }) +} + +/// Verify a grovedb indexed-axis top-k proof **and the surrounding +/// tenderdash commit**, returning the reconstructed root hash and the +/// [`RankedPage`] it commits to. +/// +/// The page is returned whole rather than as a bare entry list because +/// [`RankedPage::skipped`] is verified evidence in its own right: it is +/// re-derived from the counted subtree commitments in the proof bytes, +/// so it pins each entry to an absolute rank, and on a page past the +/// end of the ranking it is the *only* payload — an attested total +/// population under an empty entry list. +/// +/// Thin tenderdash-composition wrapper over +/// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`] in rs-drive +/// (which does the merk-level verification). Both sides derive the +/// proved subtree from the same +/// `DriveDocumentRankedQuery::indexed_property_name_tree_path`, so +/// prover and verifier cannot drift on *which* ranking is being +/// checked, and grovedb re-checks the `(axis, k, descending)` triple +/// echoed in the envelope — a proof of one ranking does not verify as +/// another. +/// +/// ## The root hash is the whole point +/// +/// The merk-level verifier returning `Ok` is **not** by itself +/// evidence of anything. Sweeping every bit of a real ranked envelope +/// shows why: most flips do error out, but roughly 9% of them (bytes +/// of sibling-subtree hashes inside the ancestor layer proofs) verify +/// cleanly and return the correct entries — under a *different* +/// reconstructed root hash. What rejects those is the +/// [`verify_tenderdash_proof`] call below, which checks the +/// reconstructed root against the quorum-signed app hash for the +/// response's block. This function exists so that composition can +/// never be skipped by accident: there is no way to obtain the entries +/// from it without the binding having run. +/// +/// The `RootHash` is returned as well, already bound, so callers can +/// log or cross-check it (e.g. against a root hash they verified for a +/// different query at the same height). Callers must not treat it as +/// something they still have to check. +pub fn verify_ranked_top_k_proof( + query: &DriveDocumentRankedQuery, + proof: &Proof, + mtd: &ResponseMetadata, + platform_version: &PlatformVersion, + provider: &dyn ContextProvider, +) -> Result<(RootHash, RankedPage), Error> { + let (root_hash, page) = query + .verify_ranked_top_k_proof(&proof.grovedb_proof, platform_version) + .map_drive_error(proof, mtd)?; + + verify_tenderdash_proof(proof, mtd, &root_hash, provider)?; + + Ok((root_hash, page)) +} + +/// Reject the generic [`FromProof`] entry point for +/// [`DocumentRankedEntries`]. +/// +/// `DocumentRankedEntries` is reached from rs-sdk via the +/// `FromProof` impl defined alongside the SDK's +/// `DocumentQuery` type (see +/// `rs-sdk/src/platform/documents/document_ranked_entries.rs`), which +/// resolves the `(axis, descending, k, offset)` tuple and the covering +/// index from the request's `(select, group_by, order_by, limit, +/// offset)` shape plus the data contract. The generic +/// `FromProof>` path carries neither — +/// `DriveDocumentQuery` has no notion of a ranking — so it errors out +/// explicitly rather than verifying the wrong thing; calling this impl +/// directly is a programmer mistake. +impl<'dq, Q> FromProof for DocumentRankedEntries +where + Q: TryInto> + Clone + 'dq, + Q::Error: std::fmt::Display, +{ + type Request = Q; + type Response = GetDocumentsResponse; + + fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( + _request: I, + _response: O, + _network: Network, + _platform_version: &PlatformVersion, + _provider: &'a dyn ContextProvider, + ) -> Result<(Option, ResponseMetadata, Proof), Error> + where + Self: 'a, + { + Err(Error::RequestError { + error: "DocumentRankedEntries can't be verified via the generic FromProof path; \ + call DocumentRankedEntries::fetch on a DocumentQuery carrying \ + .with_select(), .with_group_by(), \ + .order_by_selected_aggregate() and .with_limit(n), which \ + resolves the ranking axis and the covering index from the data contract" + .to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + //! Offline tests for the parts of the ranked surface that need + //! neither a grovedb proof nor a populated Drive: + //! + //! - the unproven `ResultData.ranked` decode, including order + //! preservation, negative fixed-point averages, and the + //! malformed-length rejection; + //! - the response-shape rejections (proof instead of data, wrong + //! `ResultData` variant, V0 response); + //! - the generic `FromProof` impl that intentionally errors to + //! prevent a silently-wrong verification. + //! + //! Proof verification itself is exercised end-to-end by rs-drive's + //! `drive_document_ranked_query::tests` (prover and verifier run + //! against a real Drive, with a bit-flip sweep asserting no tamper + //! survives with the honest root hash) and by rs-drive-abci's + //! `ranked_tests` (wire encoding of the same values). Reproducing + //! it here would need a populated Drive, which is outside this + //! crate's feature surface. + use super::*; + use dapi_grpc::platform::v0::get_documents_response::get_documents_response_v1::{ + Documents, RankedEntries, + }; + use dapi_grpc::platform::v0::get_documents_response::GetDocumentsResponseV1; + use drive::query::RANKED_AVG_SCALE; + + fn count_entry(key: &str, count: u64) -> ProtoRankedEntry { + ProtoRankedEntry { + key: key.as_bytes().to_vec(), + value: Some(ranked_entry::Value::Count(count)), + } + } + + /// An entry as the *server* would emit it for a group whose exact + /// fixed-point average is `fixed_point`: the wire carries + /// `fixed_point as f64 / RANKED_AVG_SCALE as f64`, the same + /// conversion `RankedEntryValue::as_f64` performs in rs-drive-abci. + fn avg_entry(key: &str, fixed_point: i128) -> ProtoRankedEntry { + avg_entry_raw(key, (fixed_point as f64) / (RANKED_AVG_SCALE as f64)) + } + + /// An entry carrying an arbitrary double, for the malformed cases + /// that no fixed point maps to. + fn avg_entry_raw(key: &str, avg: f64) -> ProtoRankedEntry { + ProtoRankedEntry { + key: key.as_bytes().to_vec(), + value: Some(ranked_entry::Value::Avg(avg)), + } + } + + fn response_with(result: get_documents_response_v1::Result) -> GetDocumentsResponse { + GetDocumentsResponse { + version: Some(ResponseVersion::V1(GetDocumentsResponseV1 { + result: Some(result), + metadata: Some(ResponseMetadata { + height: 42, + ..Default::default() + }), + })), + } + } + + fn ranked_response(entries: Vec) -> GetDocumentsResponse { + paged_ranked_response(entries, None) + } + + fn paged_ranked_response( + entries: Vec, + skipped: Option, + ) -> GetDocumentsResponse { + response_with(get_documents_response_v1::Result::Data(ResultData { + variant: Some(result_data::Variant::Ranked(RankedEntries { + entries, + skipped, + })), + })) + } + + /// The headline decode: `SELECT AVG(grade) … GROUP BY restaurantId + /// ORDER BY grade DESC LIMIT 3`. Entry order is the ranking order + /// and must survive the decode verbatim, and each double must + /// re-scale into the fixed point it was rendered from. + /// + /// These particular averages survive the double round trip *bit for + /// bit* — `95`, `85` and `10.5` times `10^19` all fit in `f64`'s 53 + /// significand bits — so the assertion can be exact. That is a + /// property of the fixtures, not a guarantee of the wire format: + /// `from_unproved_response` reconstructs a best-effort fixed point, + /// and an average with more significant digits than `f64` holds + /// would come back slightly off. Exactness lives on the proof path. + #[test] + fn decodes_avg_entries_preserving_ranking_order() { + let response = ranked_response(vec![ + avg_entry("gamma", 95 * RANKED_AVG_SCALE), + avg_entry("alpha", 85 * RANKED_AVG_SCALE), + // 21/2 = 10.5 — a non-integral average, so the fixed-point + // floor is actually exercised rather than a round multiple. + avg_entry("epsilon", (21 * RANKED_AVG_SCALE).div_euclid(2)), + ]); + + let (decoded, metadata) = DocumentRankedEntries::from_unproved_response(&response) + .expect("a well-formed ranked payload decodes"); + + assert_eq!(metadata.height, 42, "metadata rides along with the entries"); + let keys: Vec<&[u8]> = decoded.entries.iter().map(|e| e.key.as_slice()).collect(); + assert_eq!( + keys, + vec![ + b"gamma".as_slice(), + b"alpha".as_slice(), + b"epsilon".as_slice() + ], + "entry order is the ranking order and must not be re-sorted" + ); + assert_eq!( + decoded.entries[2].value, + RankedEntryValue::AvgFixedPoint((21 * RANKED_AVG_SCALE).div_euclid(2)) + ); + assert_eq!( + decoded.entries[2].value.as_f64(), + 10.5, + "dividing by RANKED_AVG_SCALE recovers the average a caller renders" + ); + } + + /// Averages are signed: a group whose summable property is + /// negative ranks below zero, and the sign must survive the double + /// round trip. A decoder that mishandled it would turn `-0.5` into + /// a positive number and silently invert the ranking's meaning. + /// + /// The last entry is the largest magnitude the axis can actually + /// produce — `sum = i64::MIN` over a single document — which pins + /// that the `i128`-range guard rejects only genuinely impossible + /// doubles, not legitimate extremes. + #[test] + fn decodes_negative_averages() { + let negative_half = (-RANKED_AVG_SCALE).div_euclid(2); + let extreme = (i64::MIN as i128) * RANKED_AVG_SCALE; + let response = ranked_response(vec![ + avg_entry("above", RANKED_AVG_SCALE), + avg_entry("below", negative_half), + avg_entry("floor", extreme), + ]); + + let (decoded, _) = DocumentRankedEntries::from_unproved_response(&response) + .expect("negative averages are well-formed"); + + assert_eq!( + decoded.entries[1].value, + RankedEntryValue::AvgFixedPoint(negative_half) + ); + assert_eq!(decoded.entries[1].value.as_f64(), -0.5); + assert_eq!( + decoded.entries[2].value, + RankedEntryValue::AvgFixedPoint(extreme), + "the most negative average the axis can hold is decoded, not rejected" + ); + } + + /// Count entries decode to `u64` untouched — including values + /// above 2^53, which is why the wire field is `jstype = JS_STRING`. + #[test] + fn decodes_count_entries() { + let response = ranked_response(vec![ + count_entry("delta", 4), + count_entry("beta", 3), + count_entry("huge", u64::MAX), + ]); + + let (decoded, _) = DocumentRankedEntries::from_unproved_response(&response) + .expect("count entries are well-formed"); + + assert_eq!( + decoded.entries.iter().map(|e| e.value).collect::>(), + vec![ + RankedEntryValue::Count(4), + RankedEntryValue::Count(3), + RankedEntryValue::Count(u64::MAX) + ] + ); + } + + /// Sums are signed `sint64` on the wire, so a negative running + /// total (a group of refunds) must decode as-is. + #[test] + fn decodes_signed_sum_entries() { + let response = ranked_response(vec![ProtoRankedEntry { + key: b"refunds".to_vec(), + value: Some(ranked_entry::Value::Sum(-1_000)), + }]); + + let (decoded, _) = DocumentRankedEntries::from_unproved_response(&response) + .expect("signed sums are well-formed"); + + assert_eq!(decoded.entries[0].value, RankedEntryValue::Sum(-1_000)); + } + + /// An empty ranking is a legitimate answer, not an error: the + /// index simply has no groups yet. + #[test] + fn decodes_an_empty_ranking() { + let (decoded, _) = DocumentRankedEntries::from_unproved_response(&ranked_response(vec![])) + .expect("an empty ranking is well-formed"); + assert!(decoded.entries.is_empty()); + assert_eq!(decoded.starting_rank, 0); + } + + /// `skipped` is what makes a page a ranking rather than a list. + /// A single entry at rank 4 is the *5th* best group, and the decode + /// must carry that through — dropping it would leave the caller + /// unable to distinguish it from the winner. + #[test] + fn decodes_the_starting_rank_of_a_page() { + let (decoded, _) = DocumentRankedEntries::from_unproved_response(&paged_ranked_response( + vec![avg_entry("epsilon", 10 * RANKED_AVG_SCALE)], + Some(4), + )) + .expect("a paged ranked payload decodes"); + assert_eq!(decoded.starting_rank, 4); + assert_eq!(decoded.entries.len(), 1); + } + + /// An offset past the end: no entries, but `skipped` still carries + /// the population. Empty entries plus a positive rank is a + /// meaningful answer ("there are only 12 groups"), not a + /// contradiction to be normalized away. + #[test] + fn decodes_a_page_past_the_end_of_the_ranking() { + let (decoded, _) = + DocumentRankedEntries::from_unproved_response(&paged_ranked_response(vec![], Some(12))) + .expect("a past-the-end page decodes"); + assert!(decoded.entries.is_empty()); + assert_eq!(decoded.starting_rank, 12); + } + + /// A node that predates the wire field leaves `skipped` unset. + /// That must read as rank 0 — the right answer for the offset-less + /// queries such a node could serve at all — rather than as a decode + /// failure. + #[test] + fn an_absent_skipped_field_decodes_as_rank_zero() { + let (decoded, _) = DocumentRankedEntries::from_unproved_response(&paged_ranked_response( + vec![count_entry("delta", 4)], + None, + )) + .expect("an absent `skipped` is not a decode failure"); + assert_eq!(decoded.starting_rank, 0); + } + + /// An `avg` that cannot be a scaled fixed point is rejected rather + /// than cast. `as` casts on `f64 -> i128` saturate and map `NaN` to + /// `0`, so a malformed wire value would otherwise decode as a + /// confident average of zero (or of `i128::MIN`) — a + /// plausible-looking lie is far worse than a loud failure. + /// + /// This is the double-shaped replacement for the old + /// exactly-16-bytes length check: a `double` field has no length to + /// validate, but it does have values no legitimate average maps to. + #[test] + fn rejects_an_avg_that_is_not_a_scaled_fixed_point() { + for avg in [ + f64::NAN, + f64::INFINITY, + f64::NEG_INFINITY, + // Past i128 range once multiplied by 10^19. + 1e30, + -1e30, + ] { + let response = ranked_response(vec![avg_entry_raw("alpha", avg)]); + let err = match DocumentRankedEntries::from_unproved_response(&response) { + Err(err) => err, + Ok(decoded) => panic!("an `avg` of {avg} must be rejected, decoded {decoded:?}"), + }; + let message = format!("{err}"); + assert!( + message.contains("finite double") && message.contains("i128 range"), + "the rejection must name the contract it violates; got {message}" + ); + } + } + + /// A `RankedEntry` with no `value` set means the server (or a + /// middlebox) produced a message this client cannot interpret. + /// Defaulting it to zero would fabricate a ranking position. + #[test] + fn rejects_an_entry_with_no_value() { + let response = ranked_response(vec![ProtoRankedEntry { + key: b"alpha".to_vec(), + value: None, + }]); + let err = DocumentRankedEntries::from_unproved_response(&response) + .expect_err("an entry with no value must be rejected"); + assert!(format!("{err}").contains("no `value`")); + } + + /// A proved response reaching the unproven decoder is a caller + /// mistake with a security consequence — silently returning + /// nothing (or erroring vaguely) would let a caller believe an + /// unverified path was the verified one. Name the right entry + /// point in the error. + #[test] + fn rejects_a_proof_response() { + let response = response_with(get_documents_response_v1::Result::Proof(Proof::default())); + let err = DocumentRankedEntries::from_unproved_response(&response) + .expect_err("a proof is not an unproven ranked payload"); + assert!(format!("{err}").contains("verify_ranked_top_k_proof")); + } + + /// A response on another `ResultData` variant means the node + /// routed the request somewhere else entirely (no ranking operand + /// reached it, or it landed on the count / sum / average + /// executor). Report the shape rather than an empty list. + #[test] + fn rejects_a_non_ranked_result_variant() { + let response = response_with(get_documents_response_v1::Result::Data(ResultData { + variant: Some(result_data::Variant::Documents(Documents { + documents: Vec::new(), + })), + })); + let err = DocumentRankedEntries::from_unproved_response(&response) + .expect_err("a documents payload is not a ranked one"); + assert!(format!("{err}").contains("ResultData.ranked")); + } + + /// The V0 `getDocuments` response predates the whole SQL-shaped + /// surface and has no ranked variant; a node answering V0 is a + /// node that cannot serve this query at all. + #[test] + fn rejects_a_v0_response() { + let response = GetDocumentsResponse { + version: Some(ResponseVersion::V0(Default::default())), + }; + let err = DocumentRankedEntries::from_unproved_response(&response) + .expect_err("V0 has no ranked shape"); + assert!(format!("{err}").contains("V1-only")); + } + + /// A versionless response is a decode failure, not an empty + /// ranking. + #[test] + fn rejects_a_versionless_response() { + let err = + DocumentRankedEntries::from_unproved_response(&GetDocumentsResponse { version: None }) + .expect_err("a versionless response must be rejected"); + assert!(matches!(err, Error::EmptyVersion)); + } + + /// `from_verified` is what the SDK's proof path wraps a verified + /// [`RankedPage`] with. Both halves must survive: dropping + /// `skipped` here would silently turn every proved page into a + /// claim about rank 0. + #[test] + fn from_verified_carries_both_halves_of_the_page() { + let entries = vec![ + RankedEntry { + key: b"gamma".to_vec(), + value: RankedEntryValue::Count(9), + }, + RankedEntry { + key: b"alpha".to_vec(), + value: RankedEntryValue::Count(2), + }, + ]; + let wrapped = DocumentRankedEntries::from_verified(RankedPage { + skipped: 7, + entries: entries.clone(), + }); + assert_eq!(wrapped.entries, entries); + assert_eq!( + wrapped.starting_rank, 7, + "the attested skip is the page's rank base and must not be dropped" + ); + } + + // The generic `FromProof` rejection is covered by the SDK's + // tests, which can construct a valid `DriveDocumentQuery` via + // dpp's `fixtures-and-mocks` feature. drive-proof-verifier itself + // doesn't depend on `dpp/fixtures-and-mocks` outside dev-deps for + // the contract fixtures that path needs. +} diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index 71d9b37cef4..f04f8410396 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -52,12 +52,12 @@ enum-map = { version = "2.0.3", optional = true } intmap = { version = "3.0.1", features = ["serde"], optional = true } chrono = { version = "0.4.35", optional = true } itertools = { version = "0.13", optional = true } -grovedb = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", optional = true, default-features = false } -grovedb-costs = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", optional = true } -grovedb-path = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } -grovedb-storage = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", optional = true } -grovedb-version = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } -grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } +grovedb = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", optional = true, default-features = false } +grovedb-costs = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", optional = true } +grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } +grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", optional = true } +grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } +grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } [dev-dependencies] criterion = "0.5" diff --git a/packages/rs-drive/src/cache/system_contracts.rs b/packages/rs-drive/src/cache/system_contracts.rs index 6e5f554f210..2d2655be964 100644 --- a/packages/rs-drive/src/cache/system_contracts.rs +++ b/packages/rs-drive/src/cache/system_contracts.rs @@ -415,7 +415,7 @@ mod tests { fn committing_a_block_releases_the_outgoing_protocol_version() { let contracts = SystemDataContracts::new(); - for committed_protocol_version in 9..=13 { + for committed_protocol_version in 9..=14 { // The block that switches protocol version executes at the candidate while // `check_tx` still validates against the committed one, so both are live at once. contracts @@ -440,7 +440,7 @@ mod tests { fn memoization_is_bounded_to_the_live_protocol_versions() { let contracts = SystemDataContracts::new(); - for protocol_version in [9, 10, 11, 12, 13] { + for protocol_version in [9, 10, 11, 12, 13, 14] { contracts .load_dpns(platform_version(protocol_version)) .expect("materialize DPNS"); @@ -448,7 +448,7 @@ mod tests { assert_eq!( memoized_protocol_versions(&contracts), - vec![12, 13], + vec![13, 14], "only the most recent protocol versions stay memoized" ); diff --git a/packages/rs-drive/src/drive/contract/estimation_costs/mod.rs b/packages/rs-drive/src/drive/contract/estimation_costs/mod.rs index 1748dd39b2b..a77495abc91 100644 --- a/packages/rs-drive/src/drive/contract/estimation_costs/mod.rs +++ b/packages/rs-drive/src/drive/contract/estimation_costs/mod.rs @@ -1,6 +1,9 @@ /// The estimated costs for a contract insert mod add_estimation_costs_for_contract_insertion; +use crate::drive::document::ranked_index_tree_type::{ + non_indexed_mirror_tree_type, ranked_axes_for_index_level_info, ranked_property_name_tree_type, +}; use grovedb::EstimatedSumTrees::{NoSumTrees, SomeSumTrees}; use grovedb::TreeType; @@ -45,6 +48,19 @@ impl TreeTypeWeights { self.provable_count_provable_sum = self.provable_count_provable_sum.saturating_add(1) } + // Ranked (indexed) index trees — grovedb PR 657. `EstimatedSumTrees` + // has no indexed weight slots, so each one is tallied under its + // non-indexed mirror. Byte-accurate for the per-node aggregate + // cost (an indexed primary reuses its mirror's node shape); the + // extra ~1 + 33*axes bytes an indexed *element* carries for its + // secondary root keys / axis TLV are not captured by any weight + // slot and are a known underestimate. See + // `estimated_sum_trees_for_value_tree_type` for the same note. + TreeType::ProvableCountIndexedTree + | TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + self.tally(non_indexed_mirror_tree_type(tree_type)) + } // Other tree variants (e.g. CommitmentTree) don't appear at // the doctype's children layer — they're shielded-pool-only. // Bucket them with `normal` (zero per-node aggregate cost) so @@ -93,12 +109,20 @@ impl TreeTypeWeights { /// follow the same matrix the document-storage primary-key dispatcher /// uses, see /// [`crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType::primary_key_tree_type`]. +/// +/// A terminator that additionally declares a meta-schema-v3 `ranked*` axis +/// gets the indexed mirror of whatever the table above picks — see +/// [`crate::drive::document::ranked_index_tree_type`]. Estimation is the only +/// consumer here, so a ranked index that somehow violates the +/// `ranked ⇒ range` invariant degrades to the unranked estimate rather than +/// aborting the fee calculation; the write path resolves the same pair through +/// `property_name_tree_type_and_ranked_axes` and *does* fail closed there. pub(super) fn property_name_tree_type_from_flags( info: &dpp::data_contract::document_type::IndexLevelTypeInfo, ) -> TreeType { let summable = info.summable.is_some(); let countable = info.countable.is_countable(); - match ( + let base = match ( info.range_summable, info.range_countable, summable, @@ -111,5 +135,7 @@ pub(super) fn property_name_tree_type_from_flags( (false, false, true, false) => TreeType::SumTree, (false, false, false, true) => TreeType::CountTree, (false, false, false, false) => TreeType::NormalTree, - } + }; + let ranked_axes = ranked_axes_for_index_level_info(Some(info)); + ranked_property_name_tree_type(base, &ranked_axes).unwrap_or(base) } diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 094c66b5d38..42aeab8eaec 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -1,6 +1,7 @@ use crate::drive::contract::paths; use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; +use crate::drive::document::ranked_index_tree_type::property_name_tree_type_and_ranked_axes; use crate::drive::{contract_documents_path, votes, Drive, RootTree}; use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef}; use crate::util::storage_flags::StorageFlags; @@ -396,37 +397,74 @@ impl Drive { // grovedb PR 670 combined surface): one tree // carries both metrics per-node. // - neither → NormalTree (default; matches v0). + // + // Meta schema v3 (PV14) layers the ranking axes on top: + // any `ranked*` flag upgrades the chosen variant to its + // *indexed* mirror, which additionally carries one + // ordered secondary Merk per axis. Note this dispatch + // only ever sees a TERMINAL level — `has_index_with_type` + // is `Some` exactly for single-property indexes, whose + // top-level property-name tree IS the terminal one. A + // compound index's terminal level lives deeper and is + // materialized lazily by the document index walker. let index_info = index_structure .sub_levels() .get(index.name.as_str()) .and_then(|level| level.has_index_with_type()); - let range_countable = - index_info.map(|info| info.range_countable).unwrap_or(false); - let range_summable = - index_info.map(|info| info.range_summable).unwrap_or(false); - match (range_countable, range_summable) { - (true, true) => self.batch_insert_empty_provable_count_provable_sum_tree( - type_path, - KeyRef(index_bytes), - storage_flags.as_ref(), - &mut batch_operations, - &platform_version.drive, - )?, - (true, false) => self.batch_insert_empty_provable_count_tree( - type_path, - KeyRef(index_bytes), - storage_flags.as_ref(), - &mut batch_operations, - &platform_version.drive, - )?, - (false, true) => self.batch_insert_empty_provable_sum_tree( + let (tree_type, ranked_axes) = + property_name_tree_type_and_ranked_axes(index_info)?; + match tree_type { + TreeType::ProvableCountProvableSumTree => self + .batch_insert_empty_provable_count_provable_sum_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?, + TreeType::ProvableCountTree => self + .batch_insert_empty_provable_count_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?, + TreeType::ProvableSumTree => self.batch_insert_empty_provable_sum_tree( type_path, KeyRef(index_bytes), storage_flags.as_ref(), &mut batch_operations, &platform_version.drive, )?, - (false, false) => self.batch_insert_empty_tree( + TreeType::ProvableCountIndexedTree => self + .batch_insert_empty_provable_count_indexed_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?, + TreeType::ProvableSumIndexedTree => self + .batch_insert_empty_provable_sum_indexed_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?, + TreeType::ProvableCountProvableSumIndexedTree => self + .batch_insert_empty_provable_count_provable_sum_indexed_tree( + type_path, + KeyRef(index_bytes), + &ranked_axes, + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?, + // NormalTree, and defensively anything the resolver + // could grow later: a plain subtree. + _ => self.batch_insert_empty_tree( type_path, KeyRef(index_bytes), storage_flags.as_ref(), diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/mod.rs index a6d29b9e29a..723c77e937b 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/mod.rs @@ -18,8 +18,13 @@ //! - [`shared_prefix_aggregation_e2e_tests`] — cross-index shared //! prefix layouts where a shorter aggregate index and a deeper //! range index must compose safely during document insertion. +//! - [`ranked_index_e2e_tests`] — per-index `rankedCountable` / +//! `rankedSummable` / `rankedAverageable` (meta schema v3 / PV14): +//! the indexed-tree variants and their ordered secondaries, end to +//! end through insert / update / delete. mod countable_e2e_tests; mod range_countable_index_e2e_tests; mod range_summable_index_e2e_tests; +mod ranked_index_e2e_tests; mod shared_prefix_aggregation_e2e_tests; diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs new file mode 100644 index 00000000000..b043fd6fe4e --- /dev/null +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs @@ -0,0 +1,1828 @@ +//! End-to-end coverage for **ranked aggregate indexes** (meta schema v3 / +//! PV14, grovedb PR 657 indexed trees). +//! +//! Everything here runs against the `restaurants` fixture at +//! `tests/supporting_files/contract/restaurants/restaurants-contract.json`, +//! which exercises all three indexed element variants — one per document type, +//! because two indexes over the same property set on one doctype is a +//! `DuplicateIndexError`: +//! +//! | doctype | index | declares | terminal property-name tree | +//! |----------|----------------------|--------------------------------------------|-----------------------------------------| +//! | `review` | `byRestaurant` | `averageable` + `rangeAverageable` + `rankedAverageable` | `ProvableCountProvableSumIndexedTree` axes `[Avg]` | +//! | `visit` | `byRestaurantVisits` | `countable` + `rangeCountable` + `rankedCountable` | `ProvableCountIndexedTree` | +//! | `tip` | `byRestaurantTips` | `summable` + `rangeSummable` + `rankedSummable` | `ProvableSumIndexedTree` | +//! | `adjustment` | `byRestaurantAdjustments` | same as `review` | `ProvableCountProvableSumIndexedTree` axes `[Avg]` | +//! +//! `adjustment` duplicates `review`'s shape for one reason: its aggregated +//! property `delta` admits negative values, which `grade` (minimum 0) does +//! not, so it is the only doctype that can exercise signed sums and the +//! floor-toward-negative-infinity rounding of the Avg sort key. +//! +//! Two layers are checked. First the *shape*: contract registration must lay +//! down the right indexed element, with the right axes TLV. Then the +//! *behaviour*: documents inserted through the ordinary document-insert path +//! (real grovedb batches, real secondary maintenance) must be rankable through +//! grovedb's `indexed_{avg,count,sum}_top_k` reads, and stay correct across +//! updates and deletes. +//! +//! ## Grove path from the contract root to an indexed property-name tree +//! +//! Every ranked read (and, in the query phase, every ranked proof) is issued +//! against the path of the **terminal property-name tree**. For a +//! single-property index that is: +//! +//! ```text +//! [ RootTree::DataContractDocuments as u8 ] // 0x01 +//! / +//! / [ 0x01 ] // "documents", not "contract" +//! / // e.g. b"review" +//! / // e.g. b"restaurantId" +//! ``` +//! +//! and the children of that tree are the *groups*: one value tree per distinct +//! value of the last index property, keyed by the raw index-key bytes of that +//! value (for a `string` property, its UTF-8 bytes — e.g. `b"alpha"`). The +//! secondary entries `indexed_*_top_k` returns are keyed by those same group +//! keys. A compound index `[a, b]` inserts `
/ ` between the +//! doctype and the terminal `` level. + +use crate::drive::Drive; +use crate::util::grove_operations::DirectQueryType; +use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; +use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; +use crate::util::storage_flags::StorageFlags; +use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::random_document::CreateRandomDocument; +use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; +use dpp::platform_value::Value; +use dpp::prelude::DataContract; +use dpp::tests::json_document::json_document_to_contract; +use dpp::version::PlatformVersion; +use grovedb::element::indexed::AVG_FIXED_POINT_SCALE; +use grovedb::Element; + +/// The one index property every doctype in the fixture ranks by. +const GROUP_PROPERTY: &str = "restaurantId"; + +fn platform_version() -> &'static PlatformVersion { + PlatformVersion::latest() +} + +/// Load and apply the restaurants fixture. +fn setup_restaurants() -> (Drive, DataContract) { + let drive = setup_drive_with_initial_state_structure(None); + let pv = platform_version(); + let contract = json_document_to_contract( + "tests/supporting_files/contract/restaurants/restaurants-contract.json", + false, + pv, + ) + .expect("expected to parse the restaurants contract"); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply the restaurants contract"); + (drive, contract) +} + +/// See the module docs: the path of the terminal property-name tree, which is +/// what every ranked read is issued against. +fn indexed_property_name_tree_path( + contract: &DataContract, + document_type_name: &str, +) -> Vec> { + vec![ + vec![crate::drive::RootTree::DataContractDocuments as u8], + contract.id().as_bytes().to_vec(), + vec![1], + document_type_name.as_bytes().to_vec(), + GROUP_PROPERTY.as_bytes().to_vec(), + ] +} + +/// `grove_get_raw` surfaces a missing key as `Err(PathKeyNotFound)`, so the +/// optional form is what the "group must be gone" assertions need. +fn read_grove_element(drive: &Drive, path: &[Vec], key: &[u8]) -> Option { + let path_refs: Vec<&[u8]> = path.iter().map(|v| v.as_slice()).collect(); + drive + .grove_get_raw_optional( + path_refs.as_slice().into(), + key, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version().drive, + ) + .expect("grove_get_raw_optional should succeed") +} + +/// Fetch the indexed property-name tree element itself (i.e. the element +/// living in the doctype tree under the property's key). +fn read_indexed_property_name_element( + drive: &Drive, + contract: &DataContract, + document_type_name: &str, +) -> Element { + let path = indexed_property_name_tree_path(contract, document_type_name); + let parent: Vec> = path[..path.len() - 1].to_vec(); + let key = path.last().expect("path is non-empty").clone(); + read_grove_element(drive, &parent, &key) + .unwrap_or_else(|| panic!("indexed property-name tree for {document_type_name} must exist")) +} + +/// grovedb's integrity sweep, including the per-axis primary↔secondary +/// content-consistency walk for indexed trees. Run at the end of every +/// behavioural test so a write path that keeps the reads *looking* right while +/// corrupting the secondaries still fails. +fn assert_grovedb_is_consistent(drive: &Drive) { + let issues = drive + .grove + .verify_grovedb(None, true, false, &platform_version().drive.grove_version) + .expect("verify_grovedb must run"); + assert!( + issues.is_empty(), + "grovedb integrity verification reported issues: {issues:?}" + ); +} + +/// Build a document of `document_type_name` whose properties are exactly +/// `restaurantId` plus the one aggregated integer property. +fn build_doc( + contract: &DataContract, + document_type_name: &str, + aggregated_property: &str, + restaurant: &str, + aggregated_value: i64, + seed: u64, +) -> Document { + let pv = platform_version(); + let document_type = contract + .document_type_for_name(document_type_name) + .unwrap_or_else(|_| panic!("{document_type_name} doctype exists")); + let mut doc = document_type + .random_document(Some(seed), pv) + .expect("random document"); + let mut props = std::collections::BTreeMap::new(); + props.insert( + GROUP_PROPERTY.to_string(), + Value::Text(restaurant.to_string()), + ); + props.insert( + aggregated_property.to_string(), + Value::I64(aggregated_value), + ); + doc.set_properties(props); + doc +} + +fn insert_doc(drive: &Drive, contract: &DataContract, document_type_name: &str, doc: &Document) { + let pv = platform_version(); + let document_type = contract + .document_type_for_name(document_type_name) + .unwrap_or_else(|_| panic!("{document_type_name} doctype exists")); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((doc, None)), + owner_id: None, + }, + contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .unwrap_or_else(|e| panic!("expected to insert {document_type_name} document: {e}")); +} + +/// `(restaurant, value)` pairs inserted as `document_type_name` documents, +/// returned in insertion order so tests can update / delete individual ones. +fn insert_docs( + drive: &Drive, + contract: &DataContract, + document_type_name: &str, + aggregated_property: &str, + rows: &[(&str, i64)], +) -> Vec { + rows.iter() + .enumerate() + .map(|(i, (restaurant, value))| { + let doc = build_doc( + contract, + document_type_name, + aggregated_property, + restaurant, + *value, + i as u64 + 1, + ); + insert_doc(drive, contract, document_type_name, &doc); + doc + }) + .collect() +} + +/// The fixed-point average grovedb's Avg axis orders by: +/// `floor(sum * AVG_FIXED_POINT_SCALE / count)` with euclidean (toward +/// -inf) division. The scale is grovedb's constant — 10^19 as of the +/// merged PR #657 — and is deliberately not spelled as a literal here. +/// +/// Mirrors `grovedb::element::indexed::compute_avg_fixed_point` operation +/// for operation. `div_euclid`, **not** Rust's `/`: the two agree on +/// non-negative sums and disagree by one unit on every negative sum that +/// does not divide evenly, and it is grovedb's choice that decides what is +/// on disk. `saturating_mul` matches for the same reason. +fn expected_avg_fixed_point(sum: i64, count: u64) -> i128 { + if count == 0 { + return 0; + } + (sum as i128) + .saturating_mul(AVG_FIXED_POINT_SCALE) + .div_euclid(count as i128) +} + +fn group_keys(entries: &[(T, Vec)]) -> Vec { + entries + .iter() + .map(|(_, key)| String::from_utf8(key.clone()).expect("group keys are utf-8 in this test")) + .collect() +} + +fn avg_top_k(drive: &Drive, path: &[Vec], k: u16, descending: bool) -> Vec<(i128, Vec)> { + let path_refs: Vec<&[u8]> = path.iter().map(|v| v.as_slice()).collect(); + drive + .grove + .indexed_avg_top_k( + path_refs.as_slice(), + k, + descending, + None, + &platform_version().drive.grove_version, + ) + .unwrap() + .expect("indexed_avg_top_k must succeed") +} + +fn count_top_k(drive: &Drive, path: &[Vec], k: u16, descending: bool) -> Vec<(u64, Vec)> { + let path_refs: Vec<&[u8]> = path.iter().map(|v| v.as_slice()).collect(); + drive + .grove + .indexed_count_top_k( + path_refs.as_slice(), + k, + descending, + None, + &platform_version().drive.grove_version, + ) + .unwrap() + .expect("indexed_count_top_k must succeed") +} + +fn sum_top_k(drive: &Drive, path: &[Vec], k: u16, descending: bool) -> Vec<(i64, Vec)> { + let path_refs: Vec<&[u8]> = path.iter().map(|v| v.as_slice()).collect(); + drive + .grove + .indexed_sum_top_k( + path_refs.as_slice(), + k, + descending, + None, + &platform_version().drive.grove_version, + ) + .unwrap() + .expect("indexed_sum_top_k must succeed") +} + +// --------------------------------------------------------------------------- +// Shape: contract registration lays down the declared indexed element +// --------------------------------------------------------------------------- + +/// The `rankedAverageable` index's terminal property-name tree must be a +/// `ProvableCountProvableSumIndexedTree` carrying exactly the Avg axis. The +/// element is PCPS-shaped (not PCIT/PSIT) because `rangeAverageable` puts both +/// range axes in effect — the single-axis variants cannot mirror a +/// count+sum primary. +#[test] +fn ranked_averageable_index_creates_a_pcpsit_with_only_the_avg_axis() { + let (drive, contract) = setup_restaurants(); + + match read_indexed_property_name_element(&drive, &contract, "review") { + Element::ProvableCountProvableSumIndexedTree(root_key, count, sum, axes, _) => { + assert!(root_key.is_none(), "a fresh primary has no root key"); + assert_eq!(count, 0, "a fresh primary counts nothing"); + assert_eq!(sum, 0, "a fresh primary sums nothing"); + // Avg's on-disk tag is 2; the secondary starts empty. + assert_eq!( + axes, + vec![(2u8, None)], + "rankedAverageable alone must declare exactly the Avg axis — the ranking axes \ + are independent opt-ins, so Count and Sum secondaries must NOT be created" + ); + } + other => panic!("expected ProvableCountProvableSumIndexedTree, got {other:?}"), + } +} + +/// `rankedCountable` on a count-only range layout gets the dedicated +/// single-axis PCIT element, which carries no axes TLV at all. +#[test] +fn ranked_countable_only_index_creates_a_pcit() { + let (drive, contract) = setup_restaurants(); + + match read_indexed_property_name_element(&drive, &contract, "visit") { + Element::ProvableCountIndexedTree(primary_root_key, secondary_root_key, count, _) => { + assert!(primary_root_key.is_none()); + assert!(secondary_root_key.is_none()); + assert_eq!(count, 0); + } + other => panic!("expected ProvableCountIndexedTree, got {other:?}"), + } +} + +/// `rankedSummable` on a sum-only range layout gets the dedicated single-axis +/// PSIT element. +#[test] +fn ranked_summable_only_index_creates_a_psit() { + let (drive, contract) = setup_restaurants(); + + match read_indexed_property_name_element(&drive, &contract, "tip") { + Element::ProvableSumIndexedTree(primary_root_key, secondary_root_key, sum, _) => { + assert!(primary_root_key.is_none()); + assert!(secondary_root_key.is_none()); + assert_eq!(sum, 0); + } + other => panic!("expected ProvableSumIndexedTree, got {other:?}"), + } +} + +/// The children of an indexed primary are the ordinary value trees its +/// non-indexed mirror would have — the indexed variant changes the parent, not +/// the group layout. Pinned because the write path deliberately does NOT +/// special-case value trees under an indexed parent. +#[test] +fn groups_under_an_indexed_primary_keep_their_mirror_value_tree_types() { + let (drive, contract) = setup_restaurants(); + + insert_docs(&drive, &contract, "review", "grade", &[("alpha", 50)]); + insert_docs(&drive, &contract, "visit", "guests", &[("alpha", 2)]); + insert_docs(&drive, &contract, "tip", "amount", &[("alpha", 7)]); + + // review: countable + summable + both range axes → PCPS value tree. + let review_path = indexed_property_name_tree_path(&contract, "review"); + match read_grove_element(&drive, &review_path, b"alpha").expect("group must exist") { + Element::ProvableCountProvableSumTree(_, count, sum, _) => { + assert_eq!(count, 1); + assert_eq!(sum, 50); + } + other => panic!("expected a PCPS group under the review PCPSIT, got {other:?}"), + } + + // visit: countable + rangeCountable, no sum surface → plain CountTree. + let visit_path = indexed_property_name_tree_path(&contract, "visit"); + match read_grove_element(&drive, &visit_path, b"alpha").expect("group must exist") { + Element::CountTree(_, count, _) => assert_eq!(count, 1), + other => panic!("expected a CountTree group under the visit PCIT, got {other:?}"), + } + + // tip: summable + rangeSummable, no count surface → plain SumTree. + let tip_path = indexed_property_name_tree_path(&contract, "tip"); + match read_grove_element(&drive, &tip_path, b"alpha").expect("group must exist") { + Element::SumTree(_, sum, _) => assert_eq!(sum, 7), + other => panic!("expected a SumTree group under the tip PSIT, got {other:?}"), + } + + assert_grovedb_is_consistent(&drive); +} + +// --------------------------------------------------------------------------- +// Behaviour: ranking across inserts / updates / deletes +// --------------------------------------------------------------------------- + +/// Documents inserted through the ordinary document-insert path must leave the +/// Avg secondary ordered by each restaurant's average grade. +/// +/// Averages: alpha (90+80)/2 = 85, beta (60+70+50)/3 = 60, gamma 95/1 = 95. +#[test] +fn avg_axis_ranks_groups_by_average_grade() { + let (drive, contract) = setup_restaurants(); + + insert_docs( + &drive, + &contract, + "review", + "grade", + &[ + ("alpha", 90), + ("alpha", 80), + ("beta", 60), + ("beta", 70), + ("beta", 50), + ("gamma", 95), + ], + ); + + let path = indexed_property_name_tree_path(&contract, "review"); + + let descending = avg_top_k(&drive, &path, 10, true); + assert_eq!( + group_keys(&descending), + vec!["gamma", "alpha", "beta"], + "descending Avg ranking must be gamma(95) > alpha(85) > beta(60)" + ); + assert_eq!(descending[0].0, expected_avg_fixed_point(95, 1)); + assert_eq!(descending[1].0, expected_avg_fixed_point(170, 2)); + assert_eq!(descending[2].0, expected_avg_fixed_point(180, 3)); + + let ascending = avg_top_k(&drive, &path, 10, false); + assert_eq!(group_keys(&ascending), vec!["beta", "alpha", "gamma"]); + + // `k` truncates the ranking rather than the scan — top-2 is the prefix. + let top_two = avg_top_k(&drive, &path, 2, true); + assert_eq!(group_keys(&top_two), vec!["gamma", "alpha"]); + + assert_grovedb_is_consistent(&drive); +} + +/// Averages can be negative — `summable` properties are signed — and the two +/// things that could quietly go wrong there are the fixed-point rounding and +/// the sign-aware sort-key encoding. Both are exercised here on the +/// `adjustment` doctype, whose `delta` is the one fixture property that admits +/// negative values. +/// +/// Sums: alpha (-3-4-4) = -11 over 3, beta (3+4+4) = 11 over 3, gamma 0 over +/// 1. Neither ±11/3 divides evenly, so the rounding mode is observable: +/// grovedb floors toward -inf, which for the negative group is one unit below +/// what truncating division would give. +#[test] +fn avg_axis_ranks_negative_averages_and_floors_toward_negative_infinity() { + let (drive, contract) = setup_restaurants(); + + insert_docs( + &drive, + &contract, + "adjustment", + "delta", + &[ + ("alpha", -3), + ("alpha", -4), + ("alpha", -4), + ("beta", 3), + ("beta", 4), + ("beta", 4), + ("gamma", 0), + ], + ); + + let path = indexed_property_name_tree_path(&contract, "adjustment"); + + let descending = avg_top_k(&drive, &path, 10, true); + assert_eq!( + group_keys(&descending), + vec!["beta", "gamma", "alpha"], + "the sort key must order signed averages: beta(+3.67) > gamma(0) > alpha(-3.67)" + ); + assert_eq!(descending[0].0, expected_avg_fixed_point(11, 3)); + assert_eq!(descending[1].0, expected_avg_fixed_point(0, 1)); + assert_eq!(descending[2].0, expected_avg_fixed_point(-11, 3)); + + // The rounding mode itself, stated rather than implied: on this group + // euclidean division lands one unit *below* truncating division, so a + // helper written with `/` would have agreed with the positive group and + // silently disagreed with the negative one. + let truncating = (-11i128 * AVG_FIXED_POINT_SCALE) / 3; + assert_eq!( + expected_avg_fixed_point(-11, 3), + truncating - 1, + "grovedb floors toward -inf; truncation would round the wrong way" + ); + + assert_grovedb_is_consistent(&drive); +} + +/// Updating a document's aggregated property has to move its group in the +/// ranking. This is the delete-then-reinsert secondary transition, driven +/// through the real `update_document_for_contract` path. +#[test] +fn updating_a_grade_reorders_the_avg_ranking() { + let (drive, contract) = setup_restaurants(); + let pv = platform_version(); + + let docs = insert_docs( + &drive, + &contract, + "review", + "grade", + &[("alpha", 90), ("beta", 60), ("gamma", 30)], + ); + + let path = indexed_property_name_tree_path(&contract, "review"); + assert_eq!( + group_keys(&avg_top_k(&drive, &path, 10, true)), + vec!["alpha", "beta", "gamma"], + "baseline ranking before the update" + ); + + // Push gamma from 30 to 100, which must make it the new leader. + let document_type = contract + .document_type_for_name("review") + .expect("review doctype exists"); + let mut updated = docs[2].clone(); + let mut props = updated.properties().clone(); + props.insert("grade".to_string(), Value::I64(100)); + updated.set_properties(props); + updated.set_revision(Some(2)); + + drive + .update_document_for_contract( + &updated, + &contract, + document_type, + Some(updated.owner_id().to_buffer()), + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + None, + ) + .expect("expected to update the review document"); + + assert_eq!( + group_keys(&avg_top_k(&drive, &path, 10, true)), + vec!["gamma", "alpha", "beta"], + "gamma's average moved 30 → 100 and must lead the ranking" + ); + assert_eq!( + avg_top_k(&drive, &path, 1, true)[0].0, + expected_avg_fixed_point(100, 1) + ); + + assert_grovedb_is_consistent(&drive); +} + +/// Deleting every document of a group must remove that group from the axis +/// entirely — not leave a stale zero-valued secondary entry behind. +#[test] +fn draining_a_group_removes_it_from_the_avg_axis() { + let (drive, contract) = setup_restaurants(); + let pv = platform_version(); + + let docs = insert_docs( + &drive, + &contract, + "review", + "grade", + &[("alpha", 90), ("beta", 60), ("beta", 40)], + ); + + let path = indexed_property_name_tree_path(&contract, "review"); + assert_eq!( + group_keys(&avg_top_k(&drive, &path, 10, true)), + vec!["alpha", "beta"] + ); + + // Delete one of beta's two reviews: the group survives, its average moves. + drive + .delete_document_for_contract( + docs[2].id(), + &contract, + "review", + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to delete the second beta review"); + + let after_one = avg_top_k(&drive, &path, 10, true); + assert_eq!(group_keys(&after_one), vec!["alpha", "beta"]); + assert_eq!( + after_one[1].0, + expected_avg_fixed_point(60, 1), + "beta's average must be recomputed from the surviving review alone" + ); + + // Delete beta's last review: the group drains and must disappear. + drive + .delete_document_for_contract( + docs[1].id(), + &contract, + "review", + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to delete the first beta review"); + + assert_eq!( + group_keys(&avg_top_k(&drive, &path, 10, true)), + vec!["alpha"], + "a drained group must leave the axis, not linger with a zero average" + ); + assert!( + read_grove_element(&drive, &path, b"beta").is_none(), + "the drained group's value tree must be gone from the primary too" + ); + + assert_grovedb_is_consistent(&drive); +} + +/// The PCIT arm: a count-only ranked index ranks its groups by document count. +#[test] +fn count_axis_ranks_groups_by_document_count() { + let (drive, contract) = setup_restaurants(); + + insert_docs( + &drive, + &contract, + "visit", + "guests", + &[ + ("alpha", 2), + ("beta", 4), + ("beta", 2), + ("beta", 6), + ("gamma", 3), + ("gamma", 1), + ], + ); + + let path = indexed_property_name_tree_path(&contract, "visit"); + + let descending = count_top_k(&drive, &path, 10, true); + assert_eq!( + group_keys(&descending), + vec!["beta", "gamma", "alpha"], + "descending Count ranking must be beta(3) > gamma(2) > alpha(1)" + ); + assert_eq!( + descending.iter().map(|(c, _)| *c).collect::>(), + vec![3, 2, 1] + ); + + assert_eq!( + group_keys(&count_top_k(&drive, &path, 1, false)), + vec!["alpha"], + "ascending top-1 is the smallest group" + ); + + assert_grovedb_is_consistent(&drive); +} + +// --------------------------------------------------------------------------- +// Compound ranked indexes — the terminal level is created lazily by the +// document index walker, not at contract registration +// --------------------------------------------------------------------------- + +/// Protocol version whose `document_type_schema` is 3, i.e. the first one whose +/// index grammar knows the `ranked*` keywords. +const PROTOCOL_VERSION_V14: u32 = 14; + +/// A `dish` doctype with a compound `[restaurantId, course]` ranked index. +/// `standalone_prefix_index` additionally declares a countable index +/// terminating at `[restaurantId]`, which turns the prefix's value trees into +/// aggregating `CountTree`s — the shape grovedb cannot host an indexed tree +/// inside. +fn try_build_dish_contract( + standalone_prefix_index: bool, +) -> Result { + use dpp::data_contract::DataContractFactory; + use dpp::platform_value::platform_value; + use dpp::tests::utils::generate_random_identifier_struct; + + let factory = + DataContractFactory::new(PROTOCOL_VERSION_V14).expect("expected to create factory"); + + let mut indices = vec![platform_value!({ + "name": "byRestaurantCourse", + "properties": [{"restaurantId": "asc"}, {"course": "asc"}], + "countable": "countable", + "summable": "price", + "averageable": "price", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true, + })]; + if standalone_prefix_index { + indices.push(platform_value!({ + "name": "byRestaurant", + "properties": [{"restaurantId": "asc"}], + "countable": "countable", + })); + } + + let document_schema = platform_value!({ + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "restaurantId": {"type": "string", "position": 0, "maxLength": 32}, + "course": {"type": "string", "position": 1, "maxLength": 32}, + "price": {"type": "integer", "minimum": 0, "maximum": 100000, "position": 2}, + }, + "required": ["restaurantId", "course", "price"], + "indices": Value::Array(indices), + "additionalProperties": false, + }); + + let schemas = platform_value!({ "dish": document_schema }); + + factory + .create_with_value_config(generate_random_identifier_struct(), 0, schemas, None, None) + .map(|created| created.data_contract_owned()) +} + +/// The write path's terminal-level resolver has to pick the indexed variant +/// for a **compound** ranked index too — that level lives one step below the +/// doctype tree and is materialized lazily by the document index walker, not +/// at contract registration. +/// +/// Compound ranked indexes are rejected at contract-parse time (rs-dpp, +/// single-property restriction), so the index is built as a struct literal +/// here: this pins that the drive dispatch is already correct for the day a +/// future protocol version relaxes the grammar. +#[test] +fn compound_ranked_index_resolves_its_terminal_level_to_an_indexed_tree() { + use crate::drive::document::ranked_index_tree_type::property_name_tree_type_and_ranked_axes; + use dpp::data_contract::document_type::{Index, IndexCountability, IndexLevel, IndexProperty}; + use grovedb::element::IndexAxis; + use grovedb::TreeType; + + let compound_ranked_index = Index { + name: "byRestaurantCourse".to_string(), + properties: vec![ + IndexProperty { + name: GROUP_PROPERTY.to_string(), + ascending: true, + }, + IndexProperty { + name: "course".to_string(), + ascending: true, + }, + ], + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::Countable, + range_countable: true, + summable: Some("price".to_string()), + range_summable: true, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: true, + }; + let index_structure = + IndexLevel::try_from_indices([&compound_ranked_index], "dish", platform_version()) + .expect("index level must build from a compound ranked index"); + + // Prefix level `restaurantId`: no index terminates there, so it stays a + // plain tree with no ranking axes. + let prefix_level = index_structure + .sub_levels() + .get(GROUP_PROPERTY) + .expect("prefix level exists"); + let (prefix_tree_type, prefix_axes) = + property_name_tree_type_and_ranked_axes(prefix_level.has_index_with_type()) + .expect("prefix resolution must succeed"); + assert_eq!(prefix_tree_type, TreeType::NormalTree); + assert!(prefix_axes.is_empty()); + + // Terminal level `course`: the ranked upgrade lands here. + let terminal_level = prefix_level + .sub_levels() + .get("course") + .expect("terminal level exists"); + let (terminal_tree_type, terminal_axes) = + property_name_tree_type_and_ranked_axes(terminal_level.has_index_with_type()) + .expect("terminal resolution must succeed"); + assert_eq!( + terminal_tree_type, + TreeType::ProvableCountProvableSumIndexedTree + ); + assert_eq!(terminal_axes, vec![IndexAxis::Avg]); +} + +/// Compound ranked indexes are rejected at contract-parse time (rs-dpp's +/// single-property restriction) — a v1 scope choice: a ranked terminal +/// level under an aggregating prefix would need a NonCounted/NotSummed +/// wrapper, which grovedb structurally rejects for indexed trees, and the +/// ranked query surface has no equality-prefix routing yet. (GroveDB's +/// merged PR #657 does support creating and populating an indexed tree in +/// one batch, so lazy terminal creation itself is no longer a blocker — +/// the restriction is relaxable at a future protocol version.) +/// +/// Storage-level backstop behind this gate: both wrapper dispatchers in +/// `fees/op.rs` fail closed (`DriveError::NotSupported`) for a ranked +/// terminal level inside an aggregating value tree — the frozen v0 +/// diagonal and the v14 zero-contribution matrix alike. The latter +/// matters since the v14 shared-prefix fix: its unwrapped fallback for +/// non-sum children of sum-only parents would otherwise have accepted an +/// indexed continuation, quietly creating a ranked tree that neither the +/// grammar nor the ranked query picker supports. +/// +/// Note that a ranked index *sharing* its property with a compound index +/// — `[a]` ranked next to `[a, b]` — is a different (and, since v14, +/// fully supported) shape: there the ranked level is still the terminal +/// one, and the continuation hangs *below* it. See +/// `ranked_index_ranks_correctly_next_to_a_compound_index_sharing_its_property`. +#[test] +fn compound_ranked_index_contract_is_rejected_at_parse_time() { + for standalone_prefix_index in [false, true] { + let error = try_build_dish_contract(standalone_prefix_index) + .expect_err("a compound ranked index must be rejected at contract-parse time"); + let message = error.to_string(); + assert!( + message.contains("single-property"), + "expected the single-property restriction, got: {message}" + ); + } +} + +/// Build a `visit` contract whose single-property `rankedCountable` index +/// spells `nullSearchable` out as `null_searchable`, so the rejection can be +/// exercised from the contract path rather than from the index parser alone. +fn try_build_null_searchable_ranked_contract( + null_searchable: Option, +) -> Result { + use dpp::data_contract::DataContractFactory; + use dpp::platform_value::platform_value; + use dpp::tests::utils::generate_random_identifier_struct; + + let factory = + DataContractFactory::new(PROTOCOL_VERSION_V14).expect("expected to create factory"); + + let mut index_entry: Vec<(Value, Value)> = vec![ + ( + Value::Text("name".to_string()), + Value::Text("byRestaurantVisits".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![Value::Map(vec![( + Value::Text(GROUP_PROPERTY.to_string()), + Value::Text("asc".to_string()), + )])]), + ), + ( + Value::Text("countable".to_string()), + Value::Text("countable".to_string()), + ), + (Value::Text("rangeCountable".to_string()), Value::Bool(true)), + ( + Value::Text("rankedCountable".to_string()), + Value::Bool(true), + ), + ]; + if let Some(null_searchable) = null_searchable { + index_entry.push(( + Value::Text("nullSearchable".to_string()), + Value::Bool(null_searchable), + )); + } + + let schemas = platform_value!({ + "visit": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "restaurantId": {"type": "string", "position": 0, "maxLength": 32}, + "guests": {"type": "integer", "minimum": 1, "maximum": 100, "position": 1}, + }, + "required": ["restaurantId", "guests"], + "indices": Value::Array(vec![Value::Map(index_entry)]), + "additionalProperties": false, + } + }); + + factory + .create_with_value_config(generate_random_identifier_struct(), 0, schemas, None, None) + .map(|created| created.data_contract_owned()) +} + +/// `nullSearchable: false` on a ranked index is refused at contract-parse +/// time (rs-dpp), and that parse gate is the *enforcement* — nothing below it +/// re-checks the combination. +/// +/// What the gate prevents: with `nullSearchable: false`, a document that +/// leaves the indexed property out still gets its null group's value tree +/// created by the index walker +/// (`add_indices_for_top_index_level_for_contract_operations_v2` inserts it +/// unconditionally, before recursing), and only then does the terminal +/// handler decline to write the reference +/// (`add_reference_for_index_level_for_contract_operations_v0`'s +/// `all_fields_null && !should_insert_with_all_null` early return). Under a +/// ranked index that value tree is an entry of a grovedb indexed primary, so +/// the secondary mirror records it as a group with zero aggregates — an +/// authenticated TOP/BOTTOM answer would contain a group the index is +/// supposed to exclude, with no document behind it. +/// +/// The same shape is what a ranked `[a]` sharing its prefix with `[a, b]` +/// would hit: the null group's value tree additionally hosts the compound +/// index's `b` continuation, which keeps the phantom group alive even after +/// every real document is gone. +#[test] +fn null_searchable_false_on_a_ranked_index_is_rejected_at_parse_time() { + let error = try_build_null_searchable_ranked_contract(Some(false)) + .expect_err("nullSearchable: false on a ranked index must be rejected at parse time"); + let message = error.to_string(); + assert!( + message.contains("nullSearchable") && message.contains("phantom"), + "expected the phantom-group rationale, got: {message}" + ); + + // Both accepted spellings of the default. + try_build_null_searchable_ranked_contract(None) + .expect("a ranked index with no nullSearchable key must register"); + try_build_null_searchable_ranked_contract(Some(true)) + .expect("a ranked index with an explicit nullSearchable: true must register"); +} + +/// The shape the parse gate above rules out, pinned as a struct literal — +/// the parser is the only thing standing between this combination and the +/// phantom group, so it is worth stating exactly what makes it one. +/// +/// Both halves have to be true at once for a phantom to exist, and they are: +/// +/// * `should_insert_with_all_null: false` — the terminal handler suppresses +/// the null group's reference, so the group has no document behind it; +/// * the level still resolves to an *indexed* tree, so grovedb mirrors every +/// one of its children — including that document-less null group — into the +/// ranked secondary. +/// +/// With `null_searchable: true` the first half flips: the null documents get +/// a real reference and the group becomes a legitimate rankable one. That is +/// the only suppression path into this level — the terminal handler's early +/// return is the sole place a reference is skipped, and for a single-property +/// index `all_fields_null` is exactly "this property is null" — which is why +/// the parse gate closes the hole completely rather than narrowing it. +#[test] +fn a_null_unsearchable_ranked_level_is_what_makes_a_phantom_group_possible() { + use crate::drive::document::ranked_index_tree_type::property_name_tree_type_and_ranked_axes; + use dpp::data_contract::document_type::{Index, IndexCountability, IndexLevel, IndexProperty}; + use grovedb::element::IndexAxis; + use grovedb::TreeType; + + let ranked_index = |null_searchable: bool| Index { + name: "byRestaurantVisits".to_string(), + properties: vec![IndexProperty { + name: GROUP_PROPERTY.to_string(), + ascending: true, + }], + unique: false, + null_searchable, + contested_index: None, + countable: IndexCountability::Countable, + range_countable: true, + summable: None, + range_summable: false, + ranked_countable: true, + ranked_summable: false, + ranked_averageable: false, + }; + + for null_searchable in [false, true] { + let index = ranked_index(null_searchable); + let index_structure = IndexLevel::try_from_indices([&index], "visit", platform_version()) + .expect("index level must build from a single-property ranked index"); + let terminal_level = index_structure + .sub_levels() + .get(GROUP_PROPERTY) + .expect("terminal level exists"); + let type_info = terminal_level + .has_index_with_type() + .expect("the ranked index terminates here"); + + // The level is indexed either way — that is the ranking axis doing + // its job, and it is why every child of this level reaches the + // secondary mirror. + let (tree_type, axes) = property_name_tree_type_and_ranked_axes(Some(type_info)) + .expect("terminal resolution must succeed"); + assert_eq!(tree_type, TreeType::ProvableCountIndexedTree); + assert_eq!(axes, vec![IndexAxis::Count]); + + // ...and this is the half the parse gate removes: without it the + // null group's value tree exists with no reference inside it. + assert_eq!( + type_info.should_insert_with_all_null, null_searchable, + "the terminal handler suppresses the null group's reference exactly when \ + null_searchable is false" + ); + } +} + +// --------------------------------------------------------------------------- +// Contract update +// --------------------------------------------------------------------------- + +/// Build a `cafe` contract owned by `owner_id`. With `with_ranked_doctype` a +/// second doctype carrying a single-property `rankedCountable` index is added, +/// so applying the two versions in sequence exercises +/// `update_contract_v0`'s new-doctype branch. +fn build_cafe_contract( + owner_id: dpp::identifier::Identifier, + with_ranked_doctype: bool, +) -> DataContract { + use dpp::data_contract::DataContractFactory; + use dpp::platform_value::platform_value; + + let factory = + DataContractFactory::new(PROTOCOL_VERSION_V14).expect("expected to create factory"); + + let mut schemas = vec![( + Value::Text("note".to_string()), + platform_value!({ + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "body": {"type": "string", "position": 0, "maxLength": 63}, + }, + "required": ["body"], + "additionalProperties": false, + }), + )]; + if with_ranked_doctype { + schemas.push(( + Value::Text("order".to_string()), + platform_value!({ + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "restaurantId": {"type": "string", "position": 0, "maxLength": 32}, + "guests": {"type": "integer", "minimum": 1, "maximum": 100, "position": 1}, + }, + "required": ["restaurantId", "guests"], + "indices": [{ + "name": "byRestaurantOrders", + "properties": [{"restaurantId": "asc"}], + "countable": "countable", + "rangeCountable": true, + "rankedCountable": true, + }], + "additionalProperties": false, + }), + )); + } + + factory + .create_with_value_config(owner_id, 0, Value::Map(schemas), None, None) + .expect("expected to create the cafe data contract") + .data_contract_owned() +} + +/// A contract update that introduces a doctype with a ranked index must +/// materialize the indexed element, exactly as a fresh registration would. +/// Without the ranked arms in `update_contract_v0` the new index would come +/// back as a plain `ProvableCountTree` — readable by range-count queries but +/// invisible to every ranked one, and diverging from what a fresh insert of the +/// same contract lays down. +#[test] +fn contract_update_adding_a_ranked_doctype_creates_the_indexed_element() { + use dpp::data_contract::accessors::v0::DataContractV0Setters; + + let drive = setup_drive_with_initial_state_structure(None); + let pv = platform_version(); + let owner_id = dpp::tests::utils::generate_random_identifier_struct(); + + let v1 = build_cafe_contract(owner_id, false); + drive + .apply_contract( + &v1, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply cafe v1"); + + let mut v2 = build_cafe_contract(owner_id, true); + assert_eq!(v2.id(), v1.id(), "both versions must be the same contract"); + v2.set_version(2); + drive + .apply_contract( + &v2, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply cafe v2"); + + let doctype_path: Vec> = vec![ + vec![crate::drive::RootTree::DataContractDocuments as u8], + v2.id().as_bytes().to_vec(), + vec![1], + b"order".to_vec(), + ]; + match read_grove_element(&drive, &doctype_path, GROUP_PROPERTY.as_bytes()) + .expect("the new doctype's index tree must exist after the update") + { + Element::ProvableCountIndexedTree(_, _, count, _) => assert_eq!(count, 0), + other => panic!("expected a ProvableCountIndexedTree after the update, got {other:?}"), + } + + // And it works end to end: documents inserted after the update rank. + for (restaurant, guests, seed) in [("alpha", 2, 1u64), ("beta", 4, 2), ("beta", 3, 3)] { + let document_type = v2 + .document_type_for_name("order") + .expect("order doctype exists"); + let mut doc = document_type + .random_document(Some(seed), pv) + .expect("random document"); + let mut props = std::collections::BTreeMap::new(); + props.insert( + GROUP_PROPERTY.to_string(), + Value::Text(restaurant.to_string()), + ); + props.insert("guests".to_string(), Value::I64(guests)); + doc.set_properties(props); + insert_doc(&drive, &v2, "order", &doc); + } + + let mut ranked_path = doctype_path.clone(); + ranked_path.push(GROUP_PROPERTY.as_bytes().to_vec()); + assert_eq!( + group_keys(&count_top_k(&drive, &ranked_path, 10, true)), + vec!["beta", "alpha"] + ); + + assert_grovedb_is_consistent(&drive); +} + +// --------------------------------------------------------------------------- +// Estimated (dry-run) costs +// --------------------------------------------------------------------------- + +/// Run one document insert twice — once in estimated mode (`apply: false`, +/// which populates the layer-info map and takes every stateless cost path) and +/// once applied — and return `(estimated, actual)` fees. +fn estimated_and_actual_insert_fees( + document_type_name: &str, + aggregated_property: &str, +) -> ( + dpp::fee::fee_result::FeeResult, + dpp::fee::fee_result::FeeResult, +) { + use std::borrow::Cow; + + let (drive, contract) = setup_restaurants(); + let pv = platform_version(); + let document_type = contract + .document_type_for_name(document_type_name) + .unwrap_or_else(|_| panic!("{document_type_name} doctype exists")); + let doc = build_doc( + &contract, + document_type_name, + aggregated_property, + "alpha", + 42, + 7, + ); + let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0))); + + let run = |apply: bool| { + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&doc, storage_flags.clone())), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + apply, + None, + pv, + None, + ) + .unwrap_or_else(|e| { + panic!("expected the {document_type_name} insert (apply={apply}) to succeed: {e}") + }) + }; + + // Estimate first, against the same empty state the applied run will see. + let estimated = run(false); + let actual = run(true); + (estimated, actual) +} + +/// Estimated-mode document insertion has to work end to end against a ranked +/// contract: grovedb's batch estimator understands the indexed `TreeType`s and +/// emits multi-axis `ReplaceAggregateIndexedTreeRootKeys` costs for them, so +/// the dry run must produce fees rather than erroring on an unknown layer. +/// +/// The dry run must also not *under*-charge relative to the applied write — +/// worst-case estimation is what the fee system bills against. Note the known +/// gap documented on `estimated_sum_trees_for_value_tree_type`: an indexed +/// element is ~`1 + 33*axes` bytes larger than its non-indexed mirror and no +/// `EstimatedSumTrees` weight slot captures that, so the comparison here is +/// the guard that the remaining headroom still covers it. +#[test] +fn estimated_mode_insert_on_ranked_indexes_produces_fees_and_does_not_undercharge() { + for (document_type_name, aggregated_property) in [ + ("review", "grade"), // PCPSIT, Avg axis + ("visit", "guests"), // PCIT, Count axis + ("tip", "amount"), // PSIT, Sum axis + ] { + let (estimated, actual) = + estimated_and_actual_insert_fees(document_type_name, aggregated_property); + + assert!( + estimated.storage_fee > 0, + "{document_type_name}: estimated storage fee must be non-zero" + ); + assert!( + estimated.processing_fee > 0, + "{document_type_name}: estimated processing fee must be non-zero" + ); + + assert!( + estimated.storage_fee >= actual.storage_fee, + "{document_type_name}: estimated storage fee {} is BELOW the applied storage fee {} \ + (short by {}) — the ranked layers under-charge", + estimated.storage_fee, + actual.storage_fee, + actual.storage_fee.saturating_sub(estimated.storage_fee), + ); + assert!( + estimated.processing_fee >= actual.processing_fee, + "{document_type_name}: estimated processing fee {} is BELOW the applied processing \ + fee {} (short by {}) — the ranked layers under-charge", + estimated.processing_fee, + actual.processing_fee, + actual + .processing_fee + .saturating_sub(estimated.processing_fee), + ); + } +} + +/// The PSIT arm: a sum-only ranked index ranks its groups by running sum. +#[test] +fn sum_axis_ranks_groups_by_running_sum() { + let (drive, contract) = setup_restaurants(); + + insert_docs( + &drive, + &contract, + "tip", + "amount", + &[ + ("alpha", 10), + ("alpha", 15), + ("beta", 100), + ("gamma", 7), + ("gamma", 8), + ("gamma", 9), + ], + ); + + let path = indexed_property_name_tree_path(&contract, "tip"); + + let descending = sum_top_k(&drive, &path, 10, true); + assert_eq!( + group_keys(&descending), + vec!["beta", "alpha", "gamma"], + "descending Sum ranking must be beta(100) > alpha(25) > gamma(24)" + ); + assert_eq!( + descending.iter().map(|(s, _)| *s).collect::>(), + vec![100, 25, 24] + ); + + assert_grovedb_is_consistent(&drive); +} + +// --------------------------------------------------------------------------- +// Offset pagination through the query surface +// --------------------------------------------------------------------------- +// +// Everything above reads the secondaries straight through grovedb, which is +// the right altitude for a *write-path* suite: it asserts what the insert +// path put on disk without a query grammar in between. The three tests below +// are the exception, and deliberately so — offset pagination is the one +// ranked behaviour whose answer is not fully visible in the entries. "Which +// groups came back" is observable from a raw read; "which *rank* they are" +// only exists as the proof's attested skip count, and that is produced by the +// query layer. So these run the documents through the same write path as +// everything else here and then ask the public ranked query for a page, +// end to end. + +/// `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT n +/// OFFSET m` against the fixture, unproven. Returns the page. +fn ranked_avg_page( + drive: &Drive, + contract: &DataContract, + limit: u32, + offset: u32, +) -> crate::query::RankedPage { + use crate::query::{DocumentRankedRequest, DocumentRankedResponse}; + + let response = drive + .execute_document_ranked_request( + DocumentRankedRequest { + contract, + document_type: contract + .document_type_for_name("review") + .expect("review doctype exists"), + group_by: &[GROUP_PROPERTY.to_string()], + select: crate::query::SelectProjection::avg("grade"), + having: &[], + order_by: &[crate::query::OrderClause { + field: "grade".to_string(), + ascending: false, + }], + where_clauses: &[], + limit: Some(limit), + offset: Some(offset), + has_start_at: false, + prove: false, + }, + None, + platform_version(), + ) + .expect("the ranked read must succeed"); + match response { + DocumentRankedResponse::Entries(page) => page, + DocumentRankedResponse::Proof(_) => unreachable!("prove = false"), + } +} + +/// Prove the same page and verify it, returning the **attested** page — +/// `skipped` here is re-derived by the verifier from the proof's counted +/// subtree commitments, not echoed from the request. +fn verified_ranked_avg_page( + drive: &Drive, + contract: &DataContract, + limit: u32, + offset: u32, +) -> crate::query::RankedPage { + use crate::query::drive_document_ranked_query::index_picker::find_ranked_index_for_axis; + use crate::query::{ + DocumentRankedRequest, DocumentRankedResponse, DriveDocumentRankedQuery, RankedAxis, + }; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + + let document_type = contract + .document_type_for_name("review") + .expect("review doctype exists"); + let response = drive + .execute_document_ranked_request( + DocumentRankedRequest { + contract, + document_type, + group_by: &[GROUP_PROPERTY.to_string()], + select: crate::query::SelectProjection::avg("grade"), + having: &[], + order_by: &[crate::query::OrderClause { + field: "grade".to_string(), + ascending: false, + }], + where_clauses: &[], + limit: Some(limit), + offset: Some(offset), + has_start_at: false, + prove: true, + }, + None, + platform_version(), + ) + .expect("the ranked prove must succeed"); + let proof = match response { + DocumentRankedResponse::Proof(proof) => proof, + DocumentRankedResponse::Entries(_) => unreachable!("prove = true"), + }; + + // Rebuild the query the way a client would — off the contract alone. + let indexes = contract + .document_types() + .get("review") + .expect("review doctype exists") + .indexes(); + let query = DriveDocumentRankedQuery { + document_type, + contract_id: contract.id().to_buffer(), + document_type_name: "review".to_string(), + index: find_ranked_index_for_axis(indexes, GROUP_PROPERTY, RankedAxis::Avg, "grade") + .expect("the fixture declares rankedAverageable on grade"), + axis: RankedAxis::Avg, + descending: true, + k: limit as u16, + offset, + }; + let (root_hash, page) = query + .verify_ranked_top_k_proof(&proof, platform_version()) + .expect("the proof must verify"); + assert_eq!( + root_hash, + drive + .grove + .root_hash(None, &platform_version().drive.grove_version) + .unwrap() + .expect("root hash must be readable"), + "the proof must reconstruct the live grovedb root hash" + ); + page +} + +/// Insert the five-restaurant Avg fixture used by the offset tests: +/// `gamma(95) > alpha(85) > beta(60) > delta(30) > epsilon(10)`. +fn insert_five_graded_restaurants(drive: &Drive, contract: &DataContract) { + insert_docs( + drive, + contract, + "review", + "grade", + &[ + ("gamma", 95), + ("alpha", 90), + ("alpha", 80), + ("beta", 60), + ("delta", 30), + ("epsilon", 10), + ], + ); +} + +/// **"The 5th best grade"** — `LIMIT 1 OFFSET 4`. +/// +/// The entry on its own carries no rank: `epsilon` looks identical whether +/// it was returned as the 5th-best group or as the only group in the index. +/// What makes the answer meaningful is the attested skip, which the proof +/// re-derives from the counted subtree commitments rather than echoing from +/// the request — so a server cannot answer "the 5th best" with a proof of +/// "the best". +#[test] +fn offset_four_limit_one_returns_the_fifth_best_group_with_its_attested_rank() { + let (drive, contract) = setup_restaurants(); + insert_five_graded_restaurants(&drive, &contract); + + let page = ranked_avg_page(&drive, &contract, 1, 4); + assert_eq!( + page.entries + .iter() + .map(|entry| String::from_utf8(entry.key.clone()).expect("utf-8 group keys")) + .collect::>(), + vec!["epsilon"], + "gamma > alpha > beta > delta > epsilon — rank 4 (0-based) is epsilon" + ); + assert_eq!( + page.entries[0].value, + crate::query::RankedEntryValue::AvgFixedPoint(expected_avg_fixed_point(10, 1)) + ); + + let verified = verified_ranked_avg_page(&drive, &contract, 1, 4); + assert_eq!( + verified.entries, page.entries, + "the proved page must equal the unproven one" + ); + assert_eq!( + verified.skipped, 4, + "the proof attests that exactly four groups outrank this one" + ); + + assert_grovedb_is_consistent(&drive); +} + +/// A window that runs off the end of the ranking returns the tail, short — +/// the same contract an unpaginated `LIMIT` larger than the group count has. +/// `skipped` is still the requested offset, because the *skip* succeeded; +/// only the take came up short. +#[test] +fn an_offset_window_spanning_the_end_returns_the_short_tail() { + let (drive, contract) = setup_restaurants(); + insert_five_graded_restaurants(&drive, &contract); + + let page = ranked_avg_page(&drive, &contract, 4, 3); + assert_eq!( + page.entries + .iter() + .map(|entry| String::from_utf8(entry.key.clone()).expect("utf-8 group keys")) + .collect::>(), + vec!["delta", "epsilon"], + "four were asked for from rank 3, and only two exist" + ); + + let verified = verified_ranked_avg_page(&drive, &contract, 4, 3); + assert_eq!(verified.entries, page.entries); + assert_eq!( + verified.skipped, 3, + "three groups really were skipped, so the attested skip is the requested offset" + ); + + assert_grovedb_is_consistent(&drive); +} + +/// **Paging past the end is a provable answer, not an error.** +/// +/// The page comes back empty and `skipped` collapses below the requested +/// offset — and *that shape* is the proof that the ranking holds exactly +/// `skipped` groups in total, because the counted commitments cover the whole +/// walk. It is the only way this surface reports a population, and the one +/// place the proved and unproven paths differ: the unproven read cannot see +/// the short walk (grovedb's read API returns an empty vector either way) and +/// reports the requested offset. +#[test] +fn an_offset_past_the_end_returns_an_empty_page_whose_skip_attests_the_population() { + let (drive, contract) = setup_restaurants(); + insert_five_graded_restaurants(&drive, &contract); + + let page = ranked_avg_page(&drive, &contract, 3, 12); + assert!( + page.entries.is_empty(), + "there is no rank 12 in a five-group ranking" + ); + assert_eq!( + page.skipped, 12, + "the unproven read echoes the requested offset — it has nothing to attest with" + ); + + let verified = verified_ranked_avg_page(&drive, &contract, 3, 12); + assert!(verified.entries.is_empty()); + assert_eq!( + verified.skipped, 5, + "skipped < offset with an empty page is a proof that the ranking holds exactly \ + five groups" + ); + + assert_grovedb_is_consistent(&drive); +} + +// --------------------------------------------------------------------------- +// A ranked index sharing its property with a compound index +// --------------------------------------------------------------------------- + +/// A `dish` doctype whose ranked single-property index `[restaurantId]` +/// shares its property with a plain compound index `[restaurantId, chefId]`. +/// +/// This is the shape both v14 changes meet on. The compound index hangs a +/// `chefId` continuation property-name tree inside the very value trees whose +/// (count, sum) the ranked Avg secondary orders by, so: +/// +/// * the **shared-prefix fix** decides the value trees: with a continuation +/// present they demote from `ProvableCountProvableSumTree` to +/// `CountSumTree`, and the continuation goes in `Element::NonCounted` so it +/// contributes zero to the group's count and sum; +/// * the **ranked upgrade** decides the property-name tree one level up: it +/// stays the `ProvableCountProvableSumIndexedTree` carrying the Avg axis. +/// +/// Before v14 this contract registered but rejected every document insert +/// (the diagonal-only wrapper matrix had no legal element for a plain +/// continuation under a count+sum value tree), which is why the ranked +/// multi-index fixture in `query::drive_document_ranked_query::tests` had to +/// hang its compound sibling off a count-only index instead. +fn build_shared_prefix_ranked_dish_contract() -> DataContract { + use dpp::data_contract::DataContractFactory; + use dpp::platform_value::platform_value; + use dpp::tests::utils::generate_random_identifier_struct; + + let factory = + DataContractFactory::new(PROTOCOL_VERSION_V14).expect("expected to create factory"); + + let schemas = platform_value!({ + "dish": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "restaurantId": {"type": "string", "position": 0, "maxLength": 32}, + "chefId": {"type": "string", "position": 1, "maxLength": 32}, + "grade": {"type": "integer", "minimum": 0, "maximum": 100, "position": 2}, + }, + "required": ["restaurantId", "chefId", "grade"], + "indices": [ + { + "name": "byRestaurant", + "properties": [{"restaurantId": "asc"}], + "countable": "countable", + "summable": "grade", + "averageable": "grade", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true, + }, + { + "name": "byRestaurantChef", + "properties": [{"restaurantId": "asc"}, {"chefId": "asc"}], + }, + ], + "additionalProperties": false, + } + }); + + factory + .create_with_value_config(generate_random_identifier_struct(), 0, schemas, None, None) + .expect("expected to build the shared-prefix ranked contract") + .data_contract_owned() +} + +/// Build a `dish` document with the three properties the shared-prefix +/// contract declares. +fn build_dish( + contract: &DataContract, + restaurant: &str, + chef: &str, + grade: i64, + seed: u64, +) -> Document { + let document_type = contract + .document_type_for_name("dish") + .expect("dish doctype exists"); + let mut doc = document_type + .random_document(Some(seed), platform_version()) + .expect("random document"); + let mut props = std::collections::BTreeMap::new(); + props.insert( + GROUP_PROPERTY.to_string(), + Value::Text(restaurant.to_string()), + ); + props.insert("chefId".to_string(), Value::Text(chef.to_string())); + props.insert("grade".to_string(), Value::I64(grade)); + doc.set_properties(props); + doc +} + +/// The two v14 fixes on the same doctype: ranking must stay exact while a +/// compound index hangs continuations inside the ranked groups. +/// +/// The assertions that would break if either fix regressed: +/// - inserts succeed at all (pre-v14 they did not for this shape); +/// - the Avg ranking matches the averages computed from the documents alone, +/// i.e. the `chefId` continuations contribute nothing to any group; +/// - the group's value tree is the demoted `CountSumTree` carrying exactly +/// the documents' (count, sum), with the continuation `NonCounted`-wrapped; +/// - the property-name tree above it is still the indexed element; +/// - grovedb's integrity sweep — which walks primary against secondary for +/// every axis — reports nothing. +#[test] +fn ranked_index_ranks_correctly_next_to_a_compound_index_sharing_its_property() { + let drive = setup_drive_with_initial_state_structure(None); + let contract = build_shared_prefix_ranked_dish_contract(); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version(), + ) + .expect("expected to apply the shared-prefix ranked contract"); + + // The ranked upgrade survives the presence of the compound sibling. + let element = read_indexed_property_name_element(&drive, &contract, "dish"); + match &element { + Element::ProvableCountProvableSumIndexedTree(_, _, _, axes, _) => { + assert_eq!( + axes, + &vec![(2u8, None)], + "the terminal property-name tree must carry exactly the Avg axis" + ); + } + other => panic!("expected a ProvableCountProvableSumIndexedTree, got {other:?}"), + } + + // alpha (90 + 80)/2 = 85, beta (60 + 70 + 50)/3 = 60, gamma 95/1 = 95. + // Two different chefs per restaurant, so every group really does host a + // populated `chefId` continuation. + let rows = [ + ("alpha", "chefA", 90), + ("alpha", "chefB", 80), + ("beta", "chefA", 60), + ("beta", "chefB", 70), + ("beta", "chefC", 50), + ("gamma", "chefC", 95), + ]; + let docs: Vec = rows + .iter() + .enumerate() + .map(|(i, (restaurant, chef, grade))| { + let doc = build_dish(&contract, restaurant, chef, *grade, i as u64 + 1); + insert_doc(&drive, &contract, "dish", &doc); + doc + }) + .collect(); + + let path = indexed_property_name_tree_path(&contract, "dish"); + let ranking = avg_top_k(&drive, &path, 10, true); + assert_eq!( + group_keys(&ranking), + vec!["gamma", "alpha", "beta"], + "Avg ranking must be gamma(95) > alpha(85) > beta(60)" + ); + assert_eq!( + ranking.iter().map(|(avg, _)| *avg).collect::>(), + vec![ + expected_avg_fixed_point(95, 1), + expected_avg_fixed_point(170, 2), + expected_avg_fixed_point(180, 3), + ], + "the continuations must contribute nothing to any group's (count, sum)" + ); + + // The group value tree itself: demoted to CountSumTree, carrying only the + // documents' aggregates. + let alpha = read_grove_element(&drive, &path, b"alpha").expect("alpha's group must exist"); + match &alpha { + Element::CountSumTree(_, count, sum, _) => { + assert_eq!(*count, 2, "alpha's count must be its two dishes"); + assert_eq!(*sum, 170, "alpha's sum must be 90 + 80"); + } + other => panic!("expected alpha's value tree to be a demoted CountSumTree, got {other:?}"), + } + + // ...and the compound index's continuation lives inside it, wrapped so it + // contributes zero to both axes. + let mut alpha_path = path.clone(); + alpha_path.push(b"alpha".to_vec()); + let continuation = read_grove_element(&drive, &alpha_path, b"chefId") + .expect("the compound index's continuation must exist under alpha"); + assert!( + matches!(&continuation, Element::NonCounted(inner) if matches!(inner.as_ref(), Element::Tree(..))), + "the continuation must be NonCounted-wrapped, got {continuation:?}" + ); + + // A key-changing update runs the v1 update walker over the same shape. + let document_type = contract + .document_type_for_name("dish") + .expect("dish doctype exists"); + let mut moved = docs[5].clone(); + let mut props = moved.properties().clone(); + props.insert("chefId".to_string(), Value::Text("chefD".to_string())); + props.insert("grade".to_string(), Value::I64(20)); + moved.set_properties(props); + moved.set_revision(Some(2)); + drive + .update_document_for_contract( + &moved, + &contract, + document_type, + Some(moved.owner_id().to_buffer()), + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version(), + None, + ) + .expect("expected to update the dish document across the compound index"); + + assert_eq!( + group_keys(&avg_top_k(&drive, &path, 10, true)), + vec!["alpha", "beta", "gamma"], + "gamma's only dish dropped to 20 and must fall to last" + ); + + // Deleting alpha's dishes drains the group without leaving its + // continuation behind. + for doc in docs.iter().take(2) { + drive + .delete_document_for_contract( + doc.id(), + &contract, + "dish", + BlockInfo::default(), + true, + None, + platform_version(), + None, + ) + .expect("expected to delete an alpha dish"); + } + assert_eq!( + group_keys(&avg_top_k(&drive, &path, 10, true)), + vec!["beta", "gamma"], + "the drained group must leave the axis" + ); + assert!( + read_grove_element(&drive, &path, b"alpha").is_none(), + "the drained group's value tree must be gone from the primary too" + ); + + assert_grovedb_is_consistent(&drive); +} diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs index 42e08db5171..7366e1e9a0d 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs @@ -1,4 +1,5 @@ use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; +use crate::drive::document::ranked_index_tree_type::property_name_tree_type_and_ranked_axes; use crate::drive::{contract_documents_path, Drive}; use crate::error::drive::DriveError; use crate::error::Error; @@ -326,16 +327,13 @@ impl Drive { .sub_levels() .get(index.name.as_str()) .and_then(|level| level.has_index_with_type()); - let range_countable = - index_info.map(|info| info.range_countable).unwrap_or(false); - let range_summable = - index_info.map(|info| info.range_summable).unwrap_or(false); - let target_tree_type = match (range_countable, range_summable) { - (true, true) => TreeType::ProvableCountProvableSumTree, - (true, false) => TreeType::ProvableCountTree, - (false, true) => TreeType::ProvableSumTree, - (false, false) => TreeType::NormalTree, - }; + // Meta schema v3 (PV14) additionally upgrades the + // chosen variant to its indexed mirror when the index + // declares a ranking axis; `ranked_axes` is empty for + // every pre-v3 contract, making this arm bit-identical + // to the previous 4-way dispatch for them. + let (target_tree_type, ranked_axes) = + property_name_tree_type_and_ranked_axes(index_info)?; let apply_type = if estimated_costs_only_with_layer_info.is_none() { BatchInsertTreeApplyType::StatefulBatchInsertTree } else { @@ -348,17 +346,19 @@ impl Drive { .unwrap_or_default(), } }; - // The generic `batch_insert_empty_tree_if_not_exists` - // already takes a `TreeType` arg and routes the - // grovedb insert to the matching variant — same - // helper count's non-summable index path uses. - // No-op when the path/key already exists, which is - // how this branch handles both pre-existing + // The generic `batch_insert_empty_index_tree_if_not_exists` + // already takes a `TreeType` (plus the ranking axes an + // indexed element needs and a `TreeType` cannot carry) + // and routes the grovedb insert to the matching + // variant — same helper count's non-summable index + // path uses. No-op when the path/key already exists, + // which is how this branch handles both pre-existing // indexes (unchanged on disk) and brand-new ones // (materialized with the dispatch-chosen variant). - self.batch_insert_empty_tree_if_not_exists( + self.batch_insert_empty_index_tree_if_not_exists( PathFixedSizeKeyRef((type_path, index.name.as_bytes())), target_tree_type, + &ranked_axes, storage_flags.as_ref().map(|flags| flags.as_ref()), apply_type, transaction, @@ -481,12 +481,10 @@ impl Drive { .sub_levels() .get(index.name.as_str()) .and_then(|level| level.has_index_with_type()); - let range_countable = - index_info.map(|info| info.range_countable).unwrap_or(false); - let range_summable = - index_info.map(|info| info.range_summable).unwrap_or(false); - match (range_countable, range_summable) { - (true, true) => self + let (tree_type, ranked_axes) = + property_name_tree_type_and_ranked_axes(index_info)?; + match tree_type { + TreeType::ProvableCountProvableSumTree => self .batch_insert_empty_provable_count_provable_sum_tree( type_path, KeyRef(index_bytes), @@ -494,21 +492,49 @@ impl Drive { &mut batch_operations, drive_version, )?, - (true, false) => self.batch_insert_empty_provable_count_tree( - type_path, - KeyRef(index_bytes), - storage_flags.as_ref().map(|flags| flags.as_ref()), - &mut batch_operations, - drive_version, - )?, - (false, true) => self.batch_insert_empty_provable_sum_tree( - type_path, - KeyRef(index_bytes), - storage_flags.as_ref().map(|flags| flags.as_ref()), - &mut batch_operations, - drive_version, - )?, - (false, false) => self.batch_insert_empty_tree( + TreeType::ProvableCountTree => self + .batch_insert_empty_provable_count_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?, + TreeType::ProvableSumTree => self + .batch_insert_empty_provable_sum_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?, + // Ranked (indexed) variants — meta schema v3 / PV14. + TreeType::ProvableCountIndexedTree => self + .batch_insert_empty_provable_count_indexed_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?, + TreeType::ProvableSumIndexedTree => self + .batch_insert_empty_provable_sum_indexed_tree( + type_path, + KeyRef(index_bytes), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?, + TreeType::ProvableCountProvableSumIndexedTree => self + .batch_insert_empty_provable_count_provable_sum_indexed_tree( + type_path, + KeyRef(index_bytes), + &ranked_axes, + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?, + _ => self.batch_insert_empty_tree( type_path, KeyRef(index_bytes), storage_flags.as_ref().map(|flags| flags.as_ref()), diff --git a/packages/rs-drive/src/drive/document/delete/remove_indices_for_index_level_for_contract_operations/v2/mod.rs b/packages/rs-drive/src/drive/document/delete/remove_indices_for_index_level_for_contract_operations/v2/mod.rs index 1d817df50a4..a26cf2b9047 100644 --- a/packages/rs-drive/src/drive/document/delete/remove_indices_for_index_level_for_contract_operations/v2/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/remove_indices_for_index_level_for_contract_operations/v2/mod.rs @@ -104,7 +104,12 @@ impl Drive { // fourth we need to store a reference to the document for each index for (name, sub_level) in index_level.sub_levels() { - let tree_types = index_level_tree_types_with_continuation_demotion(sub_level); + // The delete walker writes nothing itself, but its + // estimation layers must describe the tree the insert path + // actually laid down — including the meta-schema-v3 ranked + // upgrade of the property-name tree — or dry-run delete fees + // drift from applied ones on ranked indexes. + let tree_types = index_level_tree_types_with_continuation_demotion(sub_level)?; let property_name_tree_type = tree_types.property_name_tree_type; let value_tree_type = tree_types.value_tree_type; diff --git a/packages/rs-drive/src/drive/document/delete/remove_indices_for_top_index_level_for_contract_operations/v2/mod.rs b/packages/rs-drive/src/drive/document/delete/remove_indices_for_top_index_level_for_contract_operations/v2/mod.rs index 267c7f94656..884cf5306d5 100644 --- a/packages/rs-drive/src/drive/document/delete/remove_indices_for_top_index_level_for_contract_operations/v2/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/remove_indices_for_top_index_level_for_contract_operations/v2/mod.rs @@ -93,7 +93,12 @@ impl Drive { // next we need to store a reference to the document for each index for (name, sub_level) in index_level.sub_levels() { - let tree_types = index_level_tree_types_with_continuation_demotion(sub_level); + // The delete walker writes nothing itself, but its + // estimation layers must describe the tree the insert path + // actually laid down — including the meta-schema-v3 ranked + // upgrade of the property-name tree — or dry-run delete fees + // drift from applied ones on ranked indexes. + let tree_types = index_level_tree_types_with_continuation_demotion(sub_level)?; let property_name_tree_type = tree_types.property_name_tree_type; let value_tree_type = tree_types.value_tree_type; diff --git a/packages/rs-drive/src/drive/document/estimation_costs/estimated_sum_trees_for_value_tree_type.rs b/packages/rs-drive/src/drive/document/estimation_costs/estimated_sum_trees_for_value_tree_type.rs index 556209b4ea2..fe93858d294 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/estimated_sum_trees_for_value_tree_type.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/estimated_sum_trees_for_value_tree_type.rs @@ -16,6 +16,7 @@ //! helper is called from the **v1** estimators which dispatch from //! v12+ drive-version tables. +use crate::drive::document::ranked_index_tree_type::non_indexed_mirror_tree_type; use grovedb::EstimatedSumTrees::{self, NoSumTrees, SomeSumTrees}; use grovedb::TreeType; @@ -118,10 +119,75 @@ pub(crate) fn estimated_sum_trees_for_value_tree_type( provable_count_sum_trees_weight: 0, provable_count_provable_sum_trees_weight: 1, }, + // Indexed trees (grovedb PR 657) — the ranked variants. grovedb's + // `EstimatedSumTrees` has no indexed weight slots, so each indexed + // child is estimated as its non-indexed mirror. That is exact for the + // per-node cost (an indexed primary reuses the node shape, and so the + // aggregate byte cost, of the tree it mirrors) but a KNOWN + // UNDERESTIMATE of the element cost: the indexed *element* carries one + // length byte plus 33 bytes per axis (a 1-byte tag plus a 32-byte + // secondary root key) that its mirror does not, i.e. up to + // 1 + 33*3 = 100 bytes for a three-axis PCPSIT. The layer's + // `EstimatedLayerSizes` byte budget is what would have to grow to + // capture that, not these weights — recorded here so the gap is + // attributable rather than silently absorbed into a fudged weight. + // + // Reachability: a value tree is never indexed (only the terminal + // property-name tree is), so these arms fire only where an indexed + // tree is described as a *child* of another layer — the doctype layer + // during contract insertion. + TreeType::ProvableCountIndexedTree + | TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + estimated_sum_trees_for_value_tree_type(non_indexed_mirror_tree_type(value_tree_type)) + } // Defensive: any future TreeType not handled here falls back - // to `NoSumTrees`. Existing variants (NormalTree through PCPS) - // are covered above; this arm only triggers if a new variant - // is added without updating this helper. + // to `NoSumTrees`. Existing variants (NormalTree through the + // indexed trio) are covered above; this arm only triggers if a new + // variant is added without updating this helper. _ => NoSumTrees, } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Each indexed variant must land in its mirror's weight slot — and, more + /// importantly, must NOT fall through to the `NoSumTrees` default, which + /// is what the helper did before the ranked feature made these variants + /// reachable and would have under-charged every write touching one. + #[test] + fn indexed_tree_weights_land_in_their_mirrors_slot() { + let expect_slot = |tree_type: TreeType, extract: fn(&EstimatedSumTrees) -> u8| { + let weights = estimated_sum_trees_for_value_tree_type(tree_type); + assert_ne!( + weights, NoSumTrees, + "{tree_type:?} must not fall through to the NoSumTrees default" + ); + assert_eq!(extract(&weights), 1, "{tree_type:?} landed in no slot"); + }; + + expect_slot(TreeType::ProvableCountIndexedTree, |w| match w { + SomeSumTrees { + provable_count_trees_weight, + .. + } => *provable_count_trees_weight, + _ => 0, + }); + expect_slot(TreeType::ProvableSumIndexedTree, |w| match w { + SomeSumTrees { + provable_sum_trees_weight, + .. + } => *provable_sum_trees_weight, + _ => 0, + }); + expect_slot(TreeType::ProvableCountProvableSumIndexedTree, |w| match w { + SomeSumTrees { + provable_count_provable_sum_trees_weight, + .. + } => *provable_count_provable_sum_trees_weight, + _ => 0, + }); + } +} diff --git a/packages/rs-drive/src/drive/document/index_level_tree_types.rs b/packages/rs-drive/src/drive/document/index_level_tree_types.rs index f6914e5bb89..a5c9303e902 100644 --- a/packages/rs-drive/src/drive/document/index_level_tree_types.rs +++ b/packages/rs-drive/src/drive/document/index_level_tree_types.rs @@ -53,20 +53,49 @@ //! sum) into the element and contribute identically to the //! property-name tree's per-node aggregates — but the demotion means //! new writes no longer depend on the unenforced guard hole. +//! +//! ## Division of labour with `ranked_index_tree_type` +//! +//! This module owns exactly one decision: **the value-tree type**, +//! including the continuation demotion. The **property-name tree +//! type** (and the ranking axes it must carry) is owned by +//! [`crate::drive::document::ranked_index_tree_type`], which the +//! derivation below delegates to — that resolver also serves contract +//! registration, contract update, cost estimation and the query / +//! verify side, so keeping a second copy of its dispatch table here +//! would let the write path and the read path drift. +//! +//! The two decisions live one level apart and never contend: the +//! ranked upgrade replaces a property-name tree with its *indexed +//! mirror*, while the demotion only ever rewrites a value tree hanging +//! *underneath* such a tree. A demoted `CountSumTree` value tree +//! contributes its (count, sum) to an indexed parent exactly as the +//! provable variant did, so a ranked index's secondaries keep ranking +//! correctly over a shared-prefix shape. +use crate::drive::document::ranked_index_tree_type::property_name_tree_type_and_ranked_axes; +use crate::error::Error; use dpp::data_contract::document_type::IndexLevel; +use grovedb::element::IndexAxis; use grovedb::TreeType; /// The two tree types an index sub-level materializes: the /// property-name tree (keys = the property's distinct values) and the /// per-value trees underneath it (hosting the `[0]` ref-bucket plus /// any continuation property-name trees). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct IndexLevelTreeTypes { - /// Tree type of the property-name tree. Upgraded from - /// `NormalTree` only by the `range*` flags, which opt into - /// per-node aggregate commitments for range-aggregate proofs. + /// Tree type of the property-name tree. Upgraded from `NormalTree` + /// by the `range*` flags, which opt into per-node aggregate + /// commitments for range-aggregate proofs, and then by the + /// `ranked*` flags to the matching indexed mirror. Resolved by + /// [`property_name_tree_type_and_ranked_axes`]. pub property_name_tree_type: TreeType, + /// The ranking axes `property_name_tree_type` must carry. Empty for + /// every non-ranked index — i.e. for everything a pre-v14 contract + /// can express — in which case the property-name tree is a plain + /// (non-indexed) tree. + pub ranked_axes: Vec, /// Tree type of each per-value tree. Aggregating whenever the /// sub-level terminates a countable and/or summable index, with /// the continuation demotion applied (see module docs). @@ -77,36 +106,39 @@ pub(crate) struct IndexLevelTreeTypes { /// continuation demotion. This is the single source of truth for the /// v2 walkers; the v1 tables live inline in the (consensus-frozen) v1 /// walker modules. +/// +/// Fails closed when the level's ranking flags contradict its range +/// flags — see [`property_name_tree_type_and_ranked_axes`]. pub(crate) fn index_level_tree_types_with_continuation_demotion( sub_level: &IndexLevel, -) -> IndexLevelTreeTypes { +) -> Result { let info = sub_level.has_index_with_type(); - derive_index_level_tree_types( + let (property_name_tree_type, ranked_axes) = property_name_tree_type_and_ranked_axes(info)?; + let value_tree_type = derive_value_tree_type( info.map(|i| i.countable.is_countable()).unwrap_or(false), info.map(|i| i.range_countable).unwrap_or(false), info.map(|i| i.summable.is_some()).unwrap_or(false), info.map(|i| i.range_summable).unwrap_or(false), !sub_level.sub_levels().is_empty(), - ) + ); + Ok(IndexLevelTreeTypes { + property_name_tree_type, + ranked_axes, + value_tree_type, + }) } -/// Pure derivation over the level's four terminator flags plus whether -/// continuations hang beneath its value trees. Split out so the full -/// input space is unit-testable without constructing `IndexLevel`s. -fn derive_index_level_tree_types( +/// Pure derivation of the value-tree type over the level's four +/// terminator flags plus whether continuations hang beneath it. Split +/// out so the full input space is unit-testable without constructing +/// `IndexLevel`s. +fn derive_value_tree_type( countable_terminator: bool, range_countable: bool, summable_terminator: bool, range_summable: bool, has_continuations: bool, -) -> IndexLevelTreeTypes { - let property_name_tree_type = match (range_countable, range_summable) { - (true, true) => TreeType::ProvableCountProvableSumTree, - (true, false) => TreeType::ProvableCountTree, - (false, true) => TreeType::ProvableSumTree, - (false, false) => TreeType::NormalTree, - }; - +) -> TreeType { // Same dispatch table as the v1 walkers. let value_tree_type = match ( countable_terminator, @@ -124,7 +156,7 @@ fn derive_index_level_tree_types( _ => TreeType::NormalTree, }; - let value_tree_type = if has_continuations { + if has_continuations { match value_tree_type { TreeType::ProvableCountSumTree | TreeType::ProvableCountProvableSumTree => { TreeType::CountSumTree @@ -133,11 +165,6 @@ fn derive_index_level_tree_types( } } else { value_tree_type - }; - - IndexLevelTreeTypes { - property_name_tree_type, - value_tree_type, } } @@ -145,11 +172,37 @@ fn derive_index_level_tree_types( mod tests { use super::*; use crate::fees::op::LowLevelDriveOperation; + use dpp::data_contract::document_type::{IndexCountability, IndexLevelTypeInfo, IndexType}; fn all_flag_combinations() -> impl Iterator { (0u8..16).map(|bits| (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0)) } + /// A terminator level carrying the four aggregate flags and no + /// ranking axis — the only shape a pre-v14 contract can express. + fn unranked_terminator_info( + countable: bool, + range_countable: bool, + summable: bool, + range_summable: bool, + ) -> IndexLevelTypeInfo { + IndexLevelTypeInfo { + should_insert_with_all_null: false, + index_type: IndexType::NonUniqueIndex, + countable: if countable { + IndexCountability::Countable + } else { + IndexCountability::NotCountable + }, + range_countable, + summable: summable.then(|| "score".to_string()), + range_summable, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, + } + } + /// The load-bearing cross-module invariant: whenever continuations /// exist, the derived value-tree type must be accepted as a parent /// by the zero-contribution dispatcher for every continuation @@ -168,30 +221,25 @@ mod tests { ]; for (countable, range_countable, summable, range_summable) in all_flag_combinations() { - let with_continuations = derive_index_level_tree_types( - countable, - range_countable, - summable, - range_summable, - true, - ); + let with_continuations = + derive_value_tree_type(countable, range_countable, summable, range_summable, true); // Provable count-bearing value trees must never host // continuations — grovedb rejects count-suppressed // children under them. assert!( !matches!( - with_continuations.value_tree_type, + with_continuations, TreeType::ProvableCountTree | TreeType::ProvableCountSumTree | TreeType::ProvableCountProvableSumTree ), "flags ({countable}, {range_countable}, {summable}, {range_summable}): \ - value tree with continuations must not be provable count-bearing, got {:?}", - with_continuations.value_tree_type + value tree with continuations must not be provable count-bearing, got \ + {with_continuations:?}" ); - if matches!(with_continuations.value_tree_type, TreeType::NormalTree) { + if matches!(with_continuations, TreeType::NormalTree) { // Non-aggregating parents take the plain insert path. continue; } @@ -199,16 +247,15 @@ mod tests { LowLevelDriveOperation::for_known_path_key_empty_tree_contributing_zero_to_parent( vec![b"path".to_vec()], b"key".to_vec(), - with_continuations.value_tree_type, + with_continuations, continuation, None, ) .unwrap_or_else(|error| { panic!( "flags ({countable}, {range_countable}, {summable}, {range_summable}): \ - dispatcher must accept parent {:?} with continuation {continuation:?}: \ - {error}", - with_continuations.value_tree_type + dispatcher must accept parent {with_continuations:?} with continuation \ + {continuation:?}: {error}" ) }); } @@ -217,17 +264,19 @@ mod tests { /// Without continuations, the derivation must match the v1 /// walkers' (consensus-frozen) tables exactly — restated here - /// literally as the frozen expectation. + /// literally as the frozen expectation. The property-name half now + /// resolves through `ranked_index_tree_type`, so this also pins + /// that an unranked level keeps its pre-v14 property-name type. #[test] fn derivation_without_continuations_matches_v1_tables() { for (countable, range_countable, summable, range_summable) in all_flag_combinations() { - let derived = derive_index_level_tree_types( - countable, - range_countable, - summable, - range_summable, - false, - ); + let derived_value = + derive_value_tree_type(countable, range_countable, summable, range_summable, false); + let (derived_property, ranked_axes) = property_name_tree_type_and_ranked_axes(Some( + &unranked_terminator_info(countable, range_countable, summable, range_summable), + )) + .expect("unranked resolution must succeed"); + assert!(ranked_axes.is_empty()); let expected_property = match (range_countable, range_summable) { (true, true) => TreeType::ProvableCountProvableSumTree, @@ -246,8 +295,103 @@ mod tests { _ => TreeType::NormalTree, }; - assert_eq!(derived.property_name_tree_type, expected_property); - assert_eq!(derived.value_tree_type, expected_value); + assert_eq!(derived_property, expected_property); + assert_eq!(derived_value, expected_value); } } + + /// The two v14 fixes on one level: a ranked index terminating at + /// `restaurantId` next to a compound index that continues below it. + /// The ranking upgrades the property-name tree to its indexed + /// mirror, and — independently, one level down — the continuation + /// demotes the value trees to `CountSumTree`. Both must happen; a + /// change that let one suppress the other would either lose the + /// ranked secondaries or re-break document inserts on this shape. + #[test] + fn ranked_terminator_with_a_compound_continuation_gets_both_treatments() { + use dpp::data_contract::document_type::{Index, IndexProperty}; + use dpp::version::PlatformVersion; + use grovedb::element::IndexAxis; + + let ranked = Index { + name: "byRestaurant".to_string(), + properties: vec![IndexProperty { + name: "restaurantId".to_string(), + ascending: true, + }], + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::Countable, + range_countable: true, + summable: Some("grade".to_string()), + range_summable: true, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: true, + }; + let compound = Index { + name: "byRestaurantChef".to_string(), + properties: vec![ + IndexProperty { + name: "restaurantId".to_string(), + ascending: true, + }, + IndexProperty { + name: "chefId".to_string(), + ascending: true, + }, + ], + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::NotCountable, + range_countable: false, + summable: None, + range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, + }; + + let index_structure = + IndexLevel::try_from_indices([&ranked, &compound], "dish", PlatformVersion::latest()) + .expect("index level must build"); + let restaurant_level = index_structure + .sub_levels() + .get("restaurantId") + .expect("the shared prefix level exists"); + + let tree_types = index_level_tree_types_with_continuation_demotion(restaurant_level) + .expect("resolution must succeed"); + + assert_eq!( + tree_types.property_name_tree_type, + TreeType::ProvableCountProvableSumIndexedTree, + "the ranking upgrade must survive the presence of a continuation" + ); + assert_eq!(tree_types.ranked_axes, vec![IndexAxis::Avg]); + assert_eq!( + tree_types.value_tree_type, + TreeType::CountSumTree, + "the continuation must still demote the value trees under the indexed parent" + ); + + // And the demoted value tree is a legal parent for the plain + // `chefId` continuation the compound index hangs beneath it. + let chef_level = restaurant_level + .sub_levels() + .get("chefId") + .expect("the continuation level exists"); + let chef_tree_types = + index_level_tree_types_with_continuation_demotion(chef_level).expect("resolution"); + LowLevelDriveOperation::for_known_path_key_empty_tree_contributing_zero_to_parent( + vec![b"path".to_vec()], + b"chefId".to_vec(), + tree_types.value_tree_type, + chef_tree_types.property_name_tree_type, + None, + ) + .expect("the continuation must be insertable under the demoted value tree"); + } } diff --git a/packages/rs-drive/src/drive/document/insert/add_indices_for_index_level_for_contract_operations/v2/mod.rs b/packages/rs-drive/src/drive/document/insert/add_indices_for_index_level_for_contract_operations/v2/mod.rs index 183a6e8758e..4133ccdea66 100644 --- a/packages/rs-drive/src/drive/document/insert/add_indices_for_index_level_for_contract_operations/v2/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_indices_for_index_level_for_contract_operations/v2/mod.rs @@ -65,6 +65,19 @@ impl Drive { /// ref-bucket, never the structural overhead of sibling compound /// continuations. /// + /// v2 is also where the meta-schema-v3 `ranked*` grammar lands: the + /// shared helper resolves the property-name tree through + /// [`crate::drive::document::ranked_index_tree_type`], so a sub-level + /// that declares a ranking axis gets the matching *indexed* tree + /// (one ordered secondary Merk per axis) instead of the plain one. + /// The two fixes act one level apart — ranking decides the + /// property-name tree, the demotion decides the value trees beneath + /// it — and a demoted `CountSumTree` value tree contributes its + /// (count, sum) to an indexed parent exactly as the provable variant + /// did, so ranked secondaries stay correct on shared-prefix shapes. + /// `ranked_axes` is empty for every pre-v14 contract, making the + /// indexed path a bit-identical no-op for them. + /// /// See v1's docs for the underlying value-tree / property-name-tree /// design (what "countable" gates versus "range_countable", etc.); /// everything not listed above matches v1. @@ -137,8 +150,9 @@ impl Drive { // fourth we need to store a reference to the document for each index for (name, sub_level) in index_level.sub_levels() { - let tree_types = index_level_tree_types_with_continuation_demotion(sub_level); + let tree_types = index_level_tree_types_with_continuation_demotion(sub_level)?; let property_name_tree_type = tree_types.property_name_tree_type; + let ranked_axes = tree_types.ranked_axes.as_slice(); let value_tree_type = tree_types.value_tree_type; let property_name_apply_type = if estimated_costs_only_with_layer_info.is_none() { @@ -186,10 +200,19 @@ impl Drive { // here we are inserting an empty tree that will have a subtree of all other index properties if parent_value_tree_aggregates { + // A ranked terminal level reaching this branch is + // rejected inside the helper (it passes `ranked_axes` + // straight through): an indexed tree can neither be + // wrapped nor be inserted unwrapped under an + // aggregating parent, so the shape fails closed rather + // than silently degrading to a tree whose secondaries + // never exist. See `INDEXED_INNER_UNWRAPPABLE` in + // `fees::op`. self.batch_insert_empty_tree_contributing_zero_to_aggregating_parent_if_not_exists( path_key_info.clone(), parent_value_tree_type, property_name_tree_type, + ranked_axes, *storage_flags, property_name_apply_type, transaction, @@ -198,9 +221,10 @@ impl Drive { &platform_version.drive, )?; } else { - self.batch_insert_empty_tree_if_not_exists( + self.batch_insert_empty_index_tree_if_not_exists( path_key_info.clone(), property_name_tree_type, + ranked_axes, *storage_flags, property_name_apply_type, transaction, diff --git a/packages/rs-drive/src/drive/document/insert/add_indices_for_top_index_level_for_contract_operations/v2/mod.rs b/packages/rs-drive/src/drive/document/insert/add_indices_for_top_index_level_for_contract_operations/v2/mod.rs index c43ef4d9c9f..f7f465faddc 100644 --- a/packages/rs-drive/src/drive/document/insert/add_indices_for_top_index_level_for_contract_operations/v2/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_indices_for_top_index_level_for_contract_operations/v2/mod.rs @@ -97,7 +97,15 @@ impl Drive { // next we need to store a reference to the document for each index for (name, sub_level) in index_level.sub_levels() { - let tree_types = index_level_tree_types_with_continuation_demotion(sub_level); + // The top-level property-name tree is created once, at + // contract registration — this walker never writes it, so it + // has no use for `tree_types.ranked_axes`. The resolved type + // still has to include the meta-schema-v3 ranked upgrade: it + // describes the tree we insert INTO (the stateless apply + // type's `in_tree_type`) and stamps the estimation layer, and + // both must agree with what `insert_contract_operations_v0` + // laid down. + let tree_types = index_level_tree_types_with_continuation_demotion(sub_level)?; let property_name_tree_type = tree_types.property_name_tree_type; let value_tree_type = tree_types.value_tree_type; diff --git a/packages/rs-drive/src/drive/document/mod.rs b/packages/rs-drive/src/drive/document/mod.rs index d2fe57b0d10..6905d35c17c 100644 --- a/packages/rs-drive/src/drive/document/mod.rs +++ b/packages/rs-drive/src/drive/document/mod.rs @@ -53,6 +53,9 @@ pub mod paths; pub mod primary_key_tree_type; #[cfg(feature = "server")] pub(crate) mod prove; +/// Terminal property-name tree resolution for ranked (indexed-tree) indexes +#[cfg(feature = "server")] +pub(crate) mod ranked_index_tree_type; /// Shared index-walker tree-type derivation for the v2 walkers #[cfg(feature = "server")] diff --git a/packages/rs-drive/src/drive/document/ranked_index_tree_type.rs b/packages/rs-drive/src/drive/document/ranked_index_tree_type.rs new file mode 100644 index 00000000000..ca849bda392 --- /dev/null +++ b/packages/rs-drive/src/drive/document/ranked_index_tree_type.rs @@ -0,0 +1,309 @@ +//! Terminal property-name tree resolution for indexes that declare ranking +//! axes (`rankedCountable` / `rankedSummable` / `rankedAverageable`, meta +//! schema v3 / PV14). +//! +//! An index's **terminal property-name tree** is the level whose children are +//! the last index property's value trees — one child per distinct value, i.e. +//! one child per *group*. Without ranking flags its `TreeType` is picked +//! purely from `(range_countable, range_summable)`: +//! +//! | range_countable | range_summable | tree type | +//! |-----------------|----------------|--------------------------------| +//! | `true` | `true` | `ProvableCountProvableSumTree` | +//! | `true` | `false` | `ProvableCountTree` | +//! | `false` | `true` | `ProvableSumTree` | +//! | `false` | `false` | `NormalTree` | +//! +//! A ranking flag upgrades that tree to the matching *indexed* variant +//! (grovedb PR 657): a primary Merk that is a byte-compatible mirror of the +//! tree it replaces, plus one ordered secondary Merk per declared axis. Every +//! pre-existing range-aggregate read keeps working against the primary +//! unchanged; the secondaries are what make "top / bottom K groups by +//! count / sum / average" O(log n + k) with a proof. +//! +//! The upgrade table (`axes` = the declared ranking axes, canonically sorted +//! Count < Sum < Avg): +//! +//! | base | axes | indexed tree | +//! |--------------------------------|--------------|---------------------------------------| +//! | any | `[]` | unchanged (no ranking declared) | +//! | `ProvableCountTree` | `[Count]` | `ProvableCountIndexedTree` | +//! | `ProvableSumTree` | `[Sum]` | `ProvableSumIndexedTree` | +//! | `ProvableCountProvableSumTree` | any non-empty| `ProvableCountProvableSumIndexedTree` | +//! +//! The single-axis PCIT / PSIT variants carry no axis list on the element at +//! all (their one secondary is implied by the variant), which is why they are +//! only reachable when the *base* layout is already single-axis. An index that +//! declares, say, `rankedCountable` alongside `rangeSummable` still lays out +//! as PCPS underneath, so it upgrades to the multi-axis PCPSIT carrying just +//! the Count axis in its TLV. +//! +//! Every rs-drive site that needs a terminal property-name tree type — contract +//! registration, the document insert / update / delete index walkers, and the +//! cost-estimation layers — routes through +//! [`property_name_tree_type_and_ranked_axes`] so the layouts they describe +//! cannot drift apart. + +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::data_contract::document_type::IndexLevelTypeInfo; +use grovedb::element::IndexAxis; +use grovedb::TreeType; + +/// The ranking axes an index level declares, in grovedb's canonical TLV order +/// (Count < Sum < Avg, no duplicates). +/// +/// The three flags are independent opt-ins — `ranked_averageable` alone is a +/// legal (and useful) declaration, and does **not** imply the other two. +/// Levels that terminate no index, and every non-terminal prefix level, yield +/// an empty set: rs-dpp only ever populates the ranking flags on the level of +/// an index's last property. +pub(crate) fn ranked_axes_for_index_level_info( + index_level_info: Option<&IndexLevelTypeInfo>, +) -> Vec { + let Some(info) = index_level_info else { + return Vec::new(); + }; + // Pushed in ascending tag order so the resulting list is already the + // canonical TLV order `Element::validate_pcpsit_axes` demands. + let mut axes = Vec::with_capacity(3); + if info.ranked_countable { + axes.push(IndexAxis::Count); + } + if info.ranked_summable { + axes.push(IndexAxis::Sum); + } + if info.ranked_averageable { + axes.push(IndexAxis::Avg); + } + axes +} + +/// Render a canonical axis list as the `(tag, secondary_root_key)` TLV a +/// freshly created `Element::ProvableCountProvableSumIndexedTree` carries. +/// Every secondary starts empty, hence `None` for each root key. +pub(crate) fn ranked_axes_tlv(ranked_axes: &[IndexAxis]) -> Vec<(u8, Option>)> { + ranked_axes.iter().map(|axis| (axis.tag(), None)).collect() +} + +/// Upgrade the range-flag-selected `base` tree type to its indexed mirror for +/// `ranked_axes`. Returns `base` unchanged when no ranking axis is declared, +/// which is every contract written before meta schema v3. +/// +/// The `_ =>` arm is unreachable given rs-dpp's parse-time invariants +/// (`ranked_countable ⇒ range_countable`, `ranked_summable ⇒ range_summable`, +/// `ranked_averageable ⇒ both`), which force `base` to be provable on every +/// axis a ranking flag names. It is kept as a typed error rather than a +/// silent fallback so a future grammar change that breaks one of those +/// implications surfaces here instead of silently laying down a tree whose +/// secondaries nothing maintains. +pub(crate) fn ranked_property_name_tree_type( + base: TreeType, + ranked_axes: &[IndexAxis], +) -> Result { + if ranked_axes.is_empty() { + return Ok(base); + } + match (base, ranked_axes) { + (TreeType::ProvableCountTree, [IndexAxis::Count]) => Ok(TreeType::ProvableCountIndexedTree), + (TreeType::ProvableSumTree, [IndexAxis::Sum]) => Ok(TreeType::ProvableSumIndexedTree), + (TreeType::ProvableCountProvableSumTree, _) => { + Ok(TreeType::ProvableCountProvableSumIndexedTree) + } + _ => Err(Error::Drive(DriveError::CorruptedContractIndexes(format!( + "index declares ranking axes {:?} but its range flags lay the terminal \ + property-name tree out as {:?}; every ranked axis requires the matching \ + range axis (rankedCountable ⇒ rangeCountable, rankedSummable ⇒ \ + rangeSummable, rankedAverageable ⇒ both)", + ranked_axes, base + )))), + } +} + +/// One-stop resolution of a terminal property-name tree: the `TreeType` to +/// create plus the ranking axes it must carry (empty for every non-ranked +/// index). +/// +/// Callers pass the `has_index_with_type()` of the level *named after the +/// property* — `None` for pure prefix levels, which resolve to +/// `(NormalTree, [])`. +pub(crate) fn property_name_tree_type_and_ranked_axes( + index_level_info: Option<&IndexLevelTypeInfo>, +) -> Result<(TreeType, Vec), Error> { + let range_countable = index_level_info + .map(|info| info.range_countable) + .unwrap_or(false); + let range_summable = index_level_info + .map(|info| info.range_summable) + .unwrap_or(false); + let base = match (range_countable, range_summable) { + (true, true) => TreeType::ProvableCountProvableSumTree, + (true, false) => TreeType::ProvableCountTree, + (false, true) => TreeType::ProvableSumTree, + (false, false) => TreeType::NormalTree, + }; + let ranked_axes = ranked_axes_for_index_level_info(index_level_info); + Ok(( + ranked_property_name_tree_type(base, &ranked_axes)?, + ranked_axes, + )) +} + +/// The non-indexed tree type an indexed tree mirrors, or `tree_type` itself +/// when it isn't indexed. +/// +/// grovedb's indexed primaries reuse the node shape (and therefore the +/// per-node aggregate byte cost) of the tree they replace, so every place that +/// only cares about "how expensive is a node of this tree" can collapse an +/// indexed tree onto its mirror. The extra bytes an indexed tree costs live on +/// the *parent's* element (one length byte plus 33 per axis for the secondary +/// root keys / TLV), not on its nodes. +pub(crate) fn non_indexed_mirror_tree_type(tree_type: TreeType) -> TreeType { + match tree_type { + TreeType::ProvableCountIndexedTree => TreeType::ProvableCountTree, + TreeType::ProvableSumIndexedTree => TreeType::ProvableSumTree, + TreeType::ProvableCountProvableSumIndexedTree => TreeType::ProvableCountProvableSumTree, + other => other, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::data_contract::document_type::{IndexCountability, IndexType}; + + fn info( + range_countable: bool, + range_summable: bool, + ranked_countable: bool, + ranked_summable: bool, + ranked_averageable: bool, + ) -> IndexLevelTypeInfo { + IndexLevelTypeInfo { + should_insert_with_all_null: false, + index_type: IndexType::NonUniqueIndex, + countable: if range_countable { + IndexCountability::Countable + } else { + IndexCountability::NotCountable + }, + range_countable, + summable: range_summable.then(|| "score".to_string()), + range_summable, + ranked_countable, + ranked_summable, + ranked_averageable, + } + } + + /// No ranking flags ⇒ byte-identical to the pre-PV14 4-way dispatch. This + /// is the arm every existing contract takes. + #[test] + fn unranked_levels_keep_their_range_flag_tree_type() { + for (rc, rs, expected) in [ + (true, true, TreeType::ProvableCountProvableSumTree), + (true, false, TreeType::ProvableCountTree), + (false, true, TreeType::ProvableSumTree), + (false, false, TreeType::NormalTree), + ] { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(Some(&info(rc, rs, false, false, false))) + .expect("unranked resolution must succeed"); + assert_eq!(tree_type, expected); + assert!(axes.is_empty()); + } + } + + #[test] + fn prefix_levels_resolve_to_normal_tree_without_axes() { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(None).expect("prefix resolution must succeed"); + assert_eq!(tree_type, TreeType::NormalTree); + assert!(axes.is_empty()); + } + + /// Count-only ranking on a count-only range layout is the one shape that + /// gets the dedicated single-axis PCIT element. + #[test] + fn count_only_ranking_on_count_only_range_layout_is_pcit() { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(Some(&info(true, false, true, false, false))) + .expect("resolution must succeed"); + assert_eq!(tree_type, TreeType::ProvableCountIndexedTree); + assert_eq!(axes, vec![IndexAxis::Count]); + } + + #[test] + fn sum_only_ranking_on_sum_only_range_layout_is_psit() { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(Some(&info(false, true, false, true, false))) + .expect("resolution must succeed"); + assert_eq!(tree_type, TreeType::ProvableSumIndexedTree); + assert_eq!(axes, vec![IndexAxis::Sum]); + } + + /// Ranking by count only, but the index also opts into `rangeSummable`: + /// the base layout is PCPS, so the upgrade is the multi-axis PCPSIT with a + /// single-entry TLV — NOT the PCIT element (which cannot carry a + /// provable-sum primary). + #[test] + fn count_only_ranking_on_a_pcps_layout_is_pcpsit_with_one_axis() { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(Some(&info(true, true, true, false, false))) + .expect("resolution must succeed"); + assert_eq!(tree_type, TreeType::ProvableCountProvableSumIndexedTree); + assert_eq!(axes, vec![IndexAxis::Count]); + } + + /// `rankedAverageable` on its own is legal and must work. + #[test] + fn averageable_only_ranking_is_pcpsit_with_the_avg_axis() { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(Some(&info(true, true, false, false, true))) + .expect("resolution must succeed"); + assert_eq!(tree_type, TreeType::ProvableCountProvableSumIndexedTree); + assert_eq!(axes, vec![IndexAxis::Avg]); + } + + #[test] + fn all_three_axes_come_out_in_canonical_tlv_order() { + let (tree_type, axes) = + property_name_tree_type_and_ranked_axes(Some(&info(true, true, true, true, true))) + .expect("resolution must succeed"); + assert_eq!(tree_type, TreeType::ProvableCountProvableSumIndexedTree); + assert_eq!(axes, vec![IndexAxis::Count, IndexAxis::Sum, IndexAxis::Avg]); + let tlv = ranked_axes_tlv(&axes); + assert_eq!(tlv, vec![(0u8, None), (1u8, None), (2u8, None)]); + grovedb::Element::validate_pcpsit_axes(&tlv) + .expect("the TLV we emit must satisfy grovedb's canonical-axes invariant"); + } + + /// A ranked axis without its range axis is a grammar invariant violation — + /// fail closed rather than emit a tree grovedb will not maintain. + #[test] + fn ranked_axis_without_its_range_axis_is_rejected() { + let result = + property_name_tree_type_and_ranked_axes(Some(&info(false, false, true, false, false))); + assert!(result.is_err()); + } + + #[test] + fn indexed_trees_collapse_onto_their_non_indexed_mirrors() { + assert_eq!( + non_indexed_mirror_tree_type(TreeType::ProvableCountIndexedTree), + TreeType::ProvableCountTree + ); + assert_eq!( + non_indexed_mirror_tree_type(TreeType::ProvableSumIndexedTree), + TreeType::ProvableSumTree + ); + assert_eq!( + non_indexed_mirror_tree_type(TreeType::ProvableCountProvableSumIndexedTree), + TreeType::ProvableCountProvableSumTree + ); + assert_eq!( + non_indexed_mirror_tree_type(TreeType::CountSumTree), + TreeType::CountSumTree + ); + } +} diff --git a/packages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v1/mod.rs index 9be44d66288..0311b96c9cf 100644 --- a/packages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v1/mod.rs @@ -94,6 +94,16 @@ impl Drive { /// inserted them unwrapped, which would let a continuation's own /// aggregates leak into the per-value count/sum. /// + /// The same helper resolves the property-name tree through + /// [`crate::drive::document::ranked_index_tree_type`], so a branch + /// materialized here for a ranked (meta schema v3) index comes back + /// as the matching *indexed* tree with its ranking axes, not as a + /// plain tree whose secondaries nothing would maintain. A + /// key-changing update can be the operation that re-materializes a + /// property-name tree an earlier delete drained away, so it has to + /// reproduce that layout exactly. `ranked_axes` is empty for every + /// pre-v14 contract. + /// /// The `[0]` reference-bucket dispatch and everything else match v0. pub(in crate::drive::document::update) fn update_document_for_contract_operations_v1( &self, @@ -354,7 +364,7 @@ impl Drive { // materialization below still needs to know what it hangs // under. let top_level_tree_types = - index_level_tree_types_with_continuation_demotion(current_index_level); + index_level_tree_types_with_continuation_demotion(current_index_level)?; let mut parent_value_tree_type = top_level_tree_types.value_tree_type; if change_occurred_on_index { @@ -421,7 +431,7 @@ impl Drive { ))))?; let sub_level_tree_types = - index_level_tree_types_with_continuation_demotion(current_index_level); + index_level_tree_types_with_continuation_demotion(current_index_level)?; let document_index_field = document .get_raw_for_document_type( @@ -468,15 +478,23 @@ impl Drive { // aggregated axis, exactly as on the insert // path; v0 inserted it unwrapped, letting the // continuation's aggregates leak into the - // per-value count/sum. + // per-value count/sum. A ranked level resolves + // to an indexed tree here, which the + // non-aggregating branch creates with its axes + // and the aggregating branch rejects (an + // indexed tree cannot live inside an + // aggregating value tree — see + // `INDEXED_INNER_UNWRAPPABLE` in `fees::op`). let property_name_tree_type = sub_level_tree_types.property_name_tree_type; + let ranked_axes = sub_level_tree_types.ranked_axes.as_slice(); let inserted = if matches!(parent_value_tree_type, TreeType::NormalTree) { - self.batch_insert_empty_tree_if_not_exists( + self.batch_insert_empty_index_tree_if_not_exists( PathKeyInfo::PathKeyRef::<0>(( index_path.clone(), index_property.name.as_bytes(), )), property_name_tree_type, + ranked_axes, storage_flags, BatchInsertTreeApplyType::StatefulBatchInsertTree, transaction, @@ -492,6 +510,7 @@ impl Drive { )), parent_value_tree_type, property_name_tree_type, + ranked_axes, storage_flags, BatchInsertTreeApplyType::StatefulBatchInsertTree, transaction, diff --git a/packages/rs-drive/src/fees/op.rs b/packages/rs-drive/src/fees/op.rs index 65097f1fbd6..736441c13ca 100644 --- a/packages/rs-drive/src/fees/op.rs +++ b/packages/rs-drive/src/fees/op.rs @@ -9,6 +9,7 @@ use enum_map::Enum; use grovedb::batch::key_info::KeyInfo; use grovedb::batch::KeyInfoPath; use grovedb::element::reference_path::ReferencePathType; +use grovedb::element::IndexAxis; use grovedb::element::MaxReferenceHop; use grovedb::{batch::QualifiedGroveDbOp, Element, ElementFlags, TreeType}; use grovedb_costs::OperationCost; @@ -196,6 +197,10 @@ impl FunctionOp { } /// Drive operation +// GroveOperation dominates every op vec on the write path; boxing it would +// trade one inline copy for a per-op heap allocation in consensus-critical +// batching, so the size disparity against the small cost variants is accepted. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Eq, PartialEq)] pub enum LowLevelDriveOperation { /// Grove operation @@ -208,6 +213,30 @@ pub enum LowLevelDriveOperation { PreCalculatedFeeResult(FeeResult), } +/// Shared rejection message for the three `Element` wrappers +/// (`NonCounted` / `NotSummed` / `NotCountedOrSummed`) when asked to wrap an +/// indexed tree. +/// +/// grovedb's wrapper constructors reject indexed inners outright, and for a +/// structural reason rather than an oversight: the wrapper suppresses the +/// wrapped subtree's contribution to its parent's aggregate, but an indexed +/// primary's parent element is exactly where that aggregate — and the +/// secondary root keys derived from it — is committed. A wrapped indexed tree +/// would have nowhere to hang its secondaries. +/// +/// Reachable shape: a ranked index whose terminal property-name tree sits +/// inside a value tree that itself aggregates, i.e. a compound ranked index +/// `[a, b]` on a doctype that ALSO declares an aggregating index terminating +/// at `[a]`. Failing closed here is deliberate — the alternative is silently +/// writing a non-indexed tree and having ranked queries return nothing. +const INDEXED_INNER_UNWRAPPABLE: &str = + "an indexed tree cannot be wrapped in NonCounted / NotSummed / NotCountedOrSummed: the \ + wrapper suppresses the subtree's contribution to its parent's aggregate, but an indexed \ + primary commits its aggregate (and the derived secondary root keys) through that very \ + parent element. A ranked index's terminal property-name tree therefore cannot live inside \ + an aggregating value tree — i.e. a ranked compound index [a, b] is unsupported when the \ + same doctype also declares a countable/summable index terminating at [a]."; + impl LowLevelDriveOperation { /// Returns a list of the costs of the Drive operations. /// Should only be used by Calculate fee @@ -499,6 +528,13 @@ impl LowLevelDriveOperation { TreeType::ProvableCountTree => { Element::empty_provable_count_tree_with_flags(element_flags) } + TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + return Err(Error::Drive(DriveError::NotSupported( + INDEXED_INNER_UNWRAPPABLE, + ))); + } _ => { return Err(Error::Drive(DriveError::NotSupported( "NonCounted-wrapping is only supported for NormalTree, CountTree, and \ @@ -551,6 +587,13 @@ impl LowLevelDriveOperation { TreeType::ProvableCountProvableSumTree => { Element::empty_provable_count_provable_sum_tree_with_flags(element_flags) } + TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + return Err(Error::Drive(DriveError::NotSupported( + INDEXED_INNER_UNWRAPPABLE, + ))); + } _ => { return Err(Error::Drive(DriveError::NotSupported( "NotSummed-wrapping is only supported for the six sum-bearing tree \ @@ -609,7 +652,14 @@ impl LowLevelDriveOperation { // Count-only parents — wrap so the inner contributes 0 to // the parent's count. The inner can be plain or itself // count-bearing; the helper validates accepted variants. - TreeType::CountTree | TreeType::ProvableCountTree => { + // + // `ProvableCountIndexedTree` is included because an indexed + // primary aggregates exactly like the tree it mirrors: the + // wrapper choice depends on which axes the parent commits, and + // PCIT commits the same single count axis as `ProvableCountTree`. + TreeType::CountTree + | TreeType::ProvableCountTree + | TreeType::ProvableCountIndexedTree => { Self::for_known_path_key_empty_non_counted_tree( path, key, @@ -620,19 +670,21 @@ impl LowLevelDriveOperation { // Sum-only parents — wrap so the inner contributes 0 to // the parent's sum. Inner must be sum-bearing (see // `for_known_path_key_empty_not_summed_tree`'s accepted set). - TreeType::SumTree | TreeType::BigSumTree | TreeType::ProvableSumTree => { - Self::for_known_path_key_empty_not_summed_tree( - path, - key, - inner_tree_type, - storage_flags, - ) - } + TreeType::SumTree + | TreeType::BigSumTree + | TreeType::ProvableSumTree + | TreeType::ProvableSumIndexedTree => Self::for_known_path_key_empty_not_summed_tree( + path, + key, + inner_tree_type, + storage_flags, + ), // Combined count+sum parents — wrap so both axes contribute // 0. Inner must be sum-bearing. TreeType::CountSumTree | TreeType::ProvableCountSumTree - | TreeType::ProvableCountProvableSumTree => { + | TreeType::ProvableCountProvableSumTree + | TreeType::ProvableCountProvableSumIndexedTree => { Self::for_known_path_key_empty_not_counted_or_summed_tree( path, key, @@ -673,6 +725,13 @@ impl LowLevelDriveOperation { TreeType::ProvableCountProvableSumTree => { Element::empty_provable_count_provable_sum_tree_with_flags(element_flags) } + TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + return Err(Error::Drive(DriveError::NotSupported( + INDEXED_INNER_UNWRAPPABLE, + ))); + } _ => { return Err(Error::Drive(DriveError::NotSupported( "NotCountedOrSummed-wrapping is only supported for the six sum-bearing \ @@ -736,6 +795,24 @@ impl LowLevelDriveOperation { inner_tree_type: TreeType, storage_flags: Option<&StorageFlags>, ) -> Result { + // An indexed inner is rejected under every aggregating parent, + // including the sum-only ones whose non-sum fallback below is + // unwrapped: a ranked index's terminal property-name tree must + // not live inside an aggregating value tree at all (see + // `INDEXED_INNER_UNWRAPPABLE`), and letting the unwrapped + // fallback quietly accept one would create the exact shape + // rs-dpp's single-property ranked rule and the ranked query + // picker both refuse to serve. + if matches!( + inner_tree_type, + TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree + ) { + return Err(Error::Drive(DriveError::NotSupported( + INDEXED_INNER_UNWRAPPABLE, + ))); + } let inner_is_sum_bearing = matches!( inner_tree_type, TreeType::SumTree @@ -785,6 +862,24 @@ impl LowLevelDriveOperation { ) } } + // Indexed parents are structurally impossible here: the + // ranked upgrade applies to *property-name* trees, and this + // dispatcher is only ever called with a **value** tree as the + // parent. Rejected explicitly rather than through the + // non-aggregating catch-all so a future change that starts + // hanging continuations under an indexed tree reports the real + // reason (the indexed primary's secondaries are keyed by its + // children's aggregates, which a zero-contributing child would + // silently fall out of). + TreeType::ProvableCountIndexedTree + | TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + Err(Error::Drive(DriveError::NotSupported( + "indexed trees are property-name trees, never value trees, so they cannot \ + host zero-contributing continuation children — see \ + crate::drive::document::ranked_index_tree_type.", + ))) + } TreeType::ProvableCountTree | TreeType::ProvableCountSumTree | TreeType::ProvableCountProvableSumTree => { @@ -843,6 +938,13 @@ impl LowLevelDriveOperation { TreeType::ProvableCountProvableSumTree => { Element::empty_provable_count_provable_sum_tree_with_flags(element_flags) } + TreeType::ProvableSumIndexedTree + | TreeType::ProvableCountIndexedTree + | TreeType::ProvableCountProvableSumIndexedTree => { + return Err(Error::Drive(DriveError::NotSupported( + INDEXED_INNER_UNWRAPPABLE, + ))); + } _ => { return Err(Error::Drive(DriveError::NotSupported( "NonCounted-wrapping is only supported for the nine standard merk tree \ @@ -1088,6 +1190,188 @@ impl LowLevelDriveOperation { LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree) } + /// Sets `GroveOperation` for inserting an empty **provable + /// count-indexed** tree (PCIT, grovedb PR 657) at the given path and + /// key. The primary Merk is a byte-compatible mirror of + /// `ProvableCountTree`, so every existing `AggregateCountOnRange` read + /// keeps working against it; what the indexed variant adds is one + /// ordered secondary Merk keyed by `(count_be ‖ child_key)`, which is + /// what makes "top / bottom K groups by document count" O(log n + k) + /// with a proof. + /// + /// Used at contract registration (and by the index walkers when a deeper + /// level is materialized lazily) for an index that declares + /// `rankedCountable: true` while its range layout is count-only. An index + /// that also declares `rangeSummable` lays out as PCPS underneath and + /// therefore takes the multi-axis + /// [`Self::for_known_path_key_empty_provable_count_provable_sum_indexed_tree`] + /// path instead, even when Count is its only ranking axis. + pub fn for_known_path_key_empty_provable_count_indexed_tree( + path: Vec>, + key: Vec, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => Element::empty_provable_count_indexed_tree_with_flags( + storage_flags.to_some_element_flags(), + ), + None => Element::empty_provable_count_indexed_tree(), + }; + + LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree) + } + + /// Sum-axis counterpart of + /// [`Self::for_known_path_key_empty_provable_count_indexed_tree`]: an + /// empty **provable sum-indexed** tree (PSIT) whose primary mirrors + /// `ProvableSumTree` and whose single secondary is keyed by + /// `(sum_sortable_be ‖ child_key)`. + pub fn for_known_path_key_empty_provable_sum_indexed_tree( + path: Vec>, + key: Vec, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => Element::empty_provable_sum_indexed_tree_with_flags( + storage_flags.to_some_element_flags(), + ), + None => Element::empty_provable_sum_indexed_tree(), + }; + + LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree) + } + + /// Sets `GroveOperation` for inserting an empty **provable count + + /// provable sum indexed** tree (PCPSIT, grovedb PR 657) at the given path + /// and key, carrying `ranked_axes` — the canonical + /// `(axis_tag, secondary_root_key)` TLV, sorted ascending by tag with no + /// duplicates and 1..=3 entries. Every secondary starts empty, so each + /// root key is `None`. + /// + /// Unlike the two single-axis helpers this one returns a `Result`: the + /// axes list is caller-supplied and grovedb validates it + /// (`Element::validate_pcpsit_axes`), because an out-of-order, duplicated + /// or empty TLV would still be hashed into the parent via `axes_digest` + /// and produce a tree whose secondaries nothing can address. + pub fn for_known_path_key_empty_provable_count_provable_sum_indexed_tree( + path: Vec>, + key: Vec, + ranked_axes: Vec<(u8, Option>)>, + storage_flags: Option<&StorageFlags>, + ) -> Result { + let tree = match storage_flags { + Some(storage_flags) => { + Element::empty_provable_count_provable_sum_indexed_tree_with_flags( + ranked_axes, + storage_flags.to_some_element_flags(), + )? + } + None => Element::empty_provable_count_provable_sum_indexed_tree(ranked_axes)?, + }; + + Ok(LowLevelDriveOperation::insert_for_known_path_key_element( + path, key, tree, + )) + } + + /// Dispatcher over the three indexed-tree constructors, keyed on the + /// resolved `tree_type` and the ranking axes that produced it (see + /// [`crate::drive::document::ranked_index_tree_type`]). + /// + /// `ranked_axes` must be non-empty and canonical; the single-axis + /// variants additionally require that the axis matches the variant, since + /// their element shape hard-codes which aggregate the one secondary is + /// keyed by. Any other combination is an upstream resolution bug and is + /// rejected rather than silently narrowed. + pub fn for_known_path_key_empty_indexed_tree( + path: Vec>, + key: Vec, + tree_type: TreeType, + ranked_axes: &[IndexAxis], + storage_flags: Option<&StorageFlags>, + ) -> Result { + match (tree_type, ranked_axes) { + (TreeType::ProvableCountIndexedTree, [IndexAxis::Count]) => { + Ok(Self::for_known_path_key_empty_provable_count_indexed_tree( + path, + key, + storage_flags, + )) + } + (TreeType::ProvableSumIndexedTree, [IndexAxis::Sum]) => Ok( + Self::for_known_path_key_empty_provable_sum_indexed_tree(path, key, storage_flags), + ), + (TreeType::ProvableCountProvableSumIndexedTree, axes) if !axes.is_empty() => { + Self::for_known_path_key_empty_provable_count_provable_sum_indexed_tree( + path, + key, + axes.iter().map(|axis| (axis.tag(), None)).collect(), + storage_flags, + ) + } + _ => Err(Error::Drive(DriveError::NotSupported( + "for_known_path_key_empty_indexed_tree called with a tree type / ranked-axis \ + pair that does not describe an indexed tree — the single-axis PCIT / PSIT \ + variants accept exactly their own axis and PCPSIT needs a non-empty axis list.", + ))), + } + } + + /// Cost-estimation analog of + /// [`Self::for_known_path_key_empty_provable_count_indexed_tree`]. + pub fn for_estimated_path_key_empty_provable_count_indexed_tree( + path: KeyInfoPath, + key: KeyInfo, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => Element::empty_provable_count_indexed_tree_with_flags( + storage_flags.to_some_element_flags(), + ), + None => Element::empty_provable_count_indexed_tree(), + }; + + LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree) + } + + /// Cost-estimation analog of + /// [`Self::for_known_path_key_empty_provable_sum_indexed_tree`]. + pub fn for_estimated_path_key_empty_provable_sum_indexed_tree( + path: KeyInfoPath, + key: KeyInfo, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => Element::empty_provable_sum_indexed_tree_with_flags( + storage_flags.to_some_element_flags(), + ), + None => Element::empty_provable_sum_indexed_tree(), + }; + + LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree) + } + + /// Cost-estimation analog of + /// [`Self::for_known_path_key_empty_provable_count_provable_sum_indexed_tree`]. + pub fn for_estimated_path_key_empty_provable_count_provable_sum_indexed_tree( + path: KeyInfoPath, + key: KeyInfo, + ranked_axes: Vec<(u8, Option>)>, + storage_flags: Option<&StorageFlags>, + ) -> Result { + let tree = match storage_flags { + Some(storage_flags) => { + Element::empty_provable_count_provable_sum_indexed_tree_with_flags( + ranked_axes, + storage_flags.to_some_element_flags(), + )? + } + None => Element::empty_provable_count_provable_sum_indexed_tree(ranked_axes)?, + }; + + Ok(LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)) + } + /// Sets `GroveOperation` for inserting an element at the given path and key pub fn insert_for_known_path_key_element( path: Vec>, @@ -1251,6 +1535,35 @@ impl LowLevelDriveOperationTreeTypeConverter for TreeType { TreeType::DenseAppendOnlyFixedSizeTree(chunk_power) => { Element::empty_dense_tree_with_flags(*chunk_power, element_flags) } + // Single-axis indexed trees (grovedb PR 657) carry no axis list + // on the element — the one secondary Merk is implied by the + // variant — so `TreeType` alone fully describes them and the + // generic converter can build them. + TreeType::ProvableSumIndexedTree => { + Element::empty_provable_sum_indexed_tree_with_flags(element_flags) + } + TreeType::ProvableCountIndexedTree => { + Element::empty_provable_count_indexed_tree_with_flags(element_flags) + } + // The multi-axis variant is the exception: its axes TLV lives + // only on the `Element`, `TreeType` does not carry it, and an + // empty TLV is rejected by grovedb's `validate_pcpsit_axes`. The + // conversion is lossy by construction, not merely unimplemented, + // so callers must route through + // [`LowLevelDriveOperation::for_known_path_key_empty_indexed_tree`] + // (or the dedicated + // `batch_insert_empty_provable_count_provable_sum_indexed_tree` + // helper) which take the axes explicitly. Erroring here keeps a + // caller that forgot from silently emitting an axis-less indexed + // tree whose secondaries nothing maintains. + TreeType::ProvableCountProvableSumIndexedTree => { + return Err(Error::Drive(DriveError::NotSupported( + "empty_tree_operation_for_known_path_key cannot create a \ + ProvableCountProvableSumIndexedTree — the ranked axis set is not carried \ + by TreeType; use for_known_path_key_empty_indexed_tree (or \ + batch_insert_empty_provable_count_provable_sum_indexed_tree) instead.", + ))) + } }; Ok(LowLevelDriveOperation::insert_for_known_path_key_element( @@ -2270,6 +2583,37 @@ mod tests { ); } } + + // Ranked (indexed) trees are property-name trees, never value + // trees, and can never be a continuation inside an aggregating + // value tree — rejected in both roles, for every counterpart, + // including the sum-only parents whose non-sum inners are + // otherwise inserted unwrapped. + const INDEXED: [TreeType; 3] = [ + TreeType::ProvableCountIndexedTree, + TreeType::ProvableSumIndexedTree, + TreeType::ProvableCountProvableSumIndexedTree, + ]; + for indexed in INDEXED { + for inner in ALL_INNERS { + assert!( + dispatch(indexed, inner).is_err(), + "indexed parent {indexed:?} inner {inner:?} must be rejected" + ); + } + for parent in [ + TreeType::CountTree, + TreeType::CountSumTree, + TreeType::SumTree, + TreeType::BigSumTree, + TreeType::ProvableSumTree, + ] { + assert!( + dispatch(parent, indexed).is_err(), + "parent {parent:?} indexed inner {indexed:?} must be rejected" + ); + } + } } #[test] diff --git a/packages/rs-drive/src/query/drive_document_count_query/tests.rs b/packages/rs-drive/src/query/drive_document_count_query/tests.rs index 1bbac5291bb..da54e064fc9 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/tests.rs @@ -2140,6 +2140,9 @@ mod range_countable_picker_tests { // tree-shape resolver (see `primary_key_tree_type.rs`). summable: None, range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, } } diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_ranked_query/drive_dispatcher.rs new file mode 100644 index 00000000000..1783abbc925 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/drive_dispatcher.rs @@ -0,0 +1,171 @@ +//! Top-level dispatcher for the ranked +//! (`ORDER BY LIMIT n OFFSET m`) request. +//! +//! Owns the pipeline: validate → resolve the ranking → pick the covering +//! index → execute → wrap. The drive-abci handler builds a +//! [`DocumentRankedRequest`] and calls +//! [`Drive::execute_document_ranked_request`]; everything past contract +//! lookup lives here. +//! +//! Both [`DocumentRankedRequest`] and [`DocumentRankedResponse`] are the +//! ABI for this dispatcher — public so drive-abci can name the +//! input/output types without reaching into the executor surface. +//! +//! Module is gated `feature = "server"` via the parent's +//! `pub mod drive_dispatcher;` declaration. + +use super::mode_detection::detect_ranked_mode; +use super::{RankedPage, RankedPaginationInputs}; +use crate::drive::Drive; +use crate::error::Error; +use crate::query::having::HavingClause; +use crate::query::projection::SelectProjection; +use crate::query::{OrderClause, WhereClause}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +/// All inputs required by [`Drive::execute_document_ranked_request`]. +/// Built by the gRPC handler from a `GetDocumentsRequestV1` after +/// wire-decoding + contract lookup. +/// +/// The list-shaped inputs are borrowed slices rather than owned `Vec`s +/// because the dispatcher only ever reads them — nothing here is +/// canonicalized or rewritten the way count's where-clauses are, so +/// taking ownership would just force the handler into a clone. +/// +/// `where_clauses`, `having` and `start_at` are carried even though a +/// ranked request must leave all of them empty: drive owns the +/// rejection, so the contract is enforced identically no matter which +/// upstream path built the request. See +/// [`super::mode_detection::detect_ranked_mode_v0`] for why each is +/// refused rather than ignored. +pub struct DocumentRankedRequest<'a> { + /// Live contract (already loaded by the handler). + pub contract: &'a DataContract, + /// Resolved document type within `contract`. + pub document_type: DocumentTypeRef<'a>, + /// The single `GROUP BY` property. Must be the ranked index's only + /// property. + pub group_by: &'a [String], + /// The projection being ranked: `COUNT(*)`, `SUM(field)` or + /// `AVG(field)`. + pub select: SelectProjection, + /// The `HAVING` clauses. Must be empty — boolean per-group + /// predicates cannot yet be combined with an aggregate ordering. + pub having: &'a [HavingClause], + /// The `ORDER BY` clauses. Exactly one, naming the selected + /// aggregate (`$count` for `COUNT(*)`, otherwise the select's + /// field); its direction is the ranking direction. + pub order_by: &'a [OrderClause], + /// Structured `where` clauses. Must be empty. + pub where_clauses: &'a [WhereClause], + /// Request `limit` — the ranking's `k`. **Required**; there is no + /// server default a verifying client could reproduce. + pub limit: Option, + /// Request `offset` — the rank the page starts at. Optional + /// (`None` ⇒ 0), unbounded above. + pub offset: Option, + /// Whether the request carried a `start_at` / `start_after` cursor. + /// Must be `false`. + pub has_start_at: bool, + /// Whether to produce a proof instead of materializing entries. + pub prove: bool, +} + +/// Output shape of [`Drive::execute_document_ranked_request`]. +/// +/// - `Entries(RankedPage)` — the page's entries **in ranking order** +/// plus the rank it starts at; the abci handler maps this straight +/// onto the wire without re-sorting. +/// - `Proof(Vec)` — grovedb indexed-axis paginated proof bytes the +/// client verifies with +/// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof), +/// which recovers the same [`RankedPage`]. +/// +/// There is deliberately no `Aggregate` variant: even `LIMIT 1` returns +/// a one-element entry list, because the caller needs the *group* as +/// much as the value ("which restaurant is best", not just "what the +/// best score is"). +#[derive(Debug, Clone)] +pub enum DocumentRankedResponse { + /// One page of ranked groups, best-first for `DESC` and worst-first + /// for `ASC`, with the rank the page starts at. + Entries(RankedPage), + /// Grovedb indexed-axis paginated top-k proof bytes. + Proof(Vec), +} + +impl Drive { + /// Single entry point for a ranked document request. + /// + /// 1. [`detect_ranked_mode`] validates the request shape and resolves + /// `(axis, descending, k, offset, group property, aggregate + /// field)`. + /// 2. The matching executor picks the covering ranked index and runs + /// the read or the proof. + /// 3. The result is wrapped in [`DocumentRankedResponse`]. + /// + /// Errors: + /// - Request-shape failures (wrong `group_by` arity, an `order_by` + /// that does not name the `select`'s aggregate, a missing or + /// out-of-range `limit`, a `where` clause, a `having`) come back + /// as `Error::Query(QuerySyntaxError::*)` — + /// see [`super::mode_detection::detect_ranked_mode_v0`] for the + /// full grammar. + /// - "No index declares this ranking axis" comes back as + /// `Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty)` + /// naming the missing contract keyword. + /// - Everything else (grovedb, versioning) surfaces as its native + /// `Error` variant. + pub fn execute_document_ranked_request( + &self, + request: DocumentRankedRequest, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let mode = detect_ranked_mode( + &request.select, + request.group_by, + request.having, + request.order_by, + request.where_clauses, + RankedPaginationInputs { + limit: request.limit, + offset: request.offset, + has_start_at: request.has_start_at, + }, + platform_version, + )?; + + let contract_id = request.contract.id_ref().to_buffer(); + let document_type_name = request.document_type.name().to_string(); + + if request.prove { + Ok(DocumentRankedResponse::Proof( + self.execute_document_ranked_top_k_proof( + contract_id, + request.document_type, + document_type_name, + &mode, + transaction, + platform_version, + )?, + )) + } else { + Ok(DocumentRankedResponse::Entries( + self.execute_document_ranked_top_k_no_proof( + contract_id, + request.document_type, + document_type_name, + &mode, + transaction, + platform_version, + )?, + )) + } + } +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs b/packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs new file mode 100644 index 00000000000..fb31a6635ff --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs @@ -0,0 +1,193 @@ +//! The two ranked executors on [`DriveDocumentRankedQuery`]: a direct +//! read of the axis secondary, and generation of the equivalent proof. +//! +//! Both are thin — all of the work happens inside grovedb, which walks +//! the pre-sorted secondary Merk directly. That is the whole point of the +//! ranked surface: no value trees are opened, no documents are +//! materialized, and the cost is `O(log n + k)` rather than +//! `O(groups × log n)`. +//! +//! Whole module is gated `feature = "server"` via the parent's +//! `pub mod execute_top_k;` declaration. + +use super::{DriveDocumentRankedQuery, RankedAxis, RankedEntry, RankedEntryValue, RankedPage}; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; +use grovedb_costs::CostContext; + +impl DriveDocumentRankedQuery<'_> { + /// Read one page of the ranking directly from the axis secondary: + /// the `k` groups starting at rank `offset`. Entries come back in + /// ranking order — see [`DriveDocumentRankedQuery::descending`] for + /// the direction and the tie contract. + /// + /// Fewer than `k` entries is normal (the index simply has fewer + /// groups than `offset + k`) and is not an error. A missing path + /// *is* an error rather than an empty result: the indexed + /// property-name tree is created when the contract is registered, so + /// its absence means the contract-level state is not what the + /// request claims, not that the ranking is empty. (An index with no + /// documents yet has the tree, with an empty secondary, and yields + /// an empty entry list.) + /// + /// The paginated grovedb primitive is used unconditionally, with + /// `offset = 0` standing in for an unpaginated request, so the + /// no-proof and prove paths read the same code path in grovedb and + /// cannot drift on the walk's semantics for offset-free queries. + /// + /// [`RankedPage::skipped`] on this path is the *requested* offset: + /// grovedb's read API returns an empty vector when the walk runs out + /// during the skip and does not report how far it got, so an + /// unproven read cannot distinguish "skipped exactly `offset`" from + /// "the secondary holds fewer than `offset` groups". Only the proved + /// path attests the true value — see [`RankedPage::skipped`]. + pub fn execute_top_k_no_proof( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let grove_version = &platform_version.drive.grove_version; + let path = self.indexed_property_name_tree_path()?; + let path_refs: Vec<&[u8]> = path.iter().map(|segment| segment.as_slice()).collect(); + let offset = self.offset as u64; + + // Costs are destructured away rather than `.unwrap()`-ed: + // `CostContext::unwrap` is infallible (it drops the cost field) + // but reads like a panicking unwrap at the call site. The + // dispatcher wraps these executors with its own fee accounting, + // exactly as the count surface's `execute_range_count_no_proof` + // does. + let entries = match self.axis { + RankedAxis::Count => { + let CostContext { value, cost: _ } = drive.grove.indexed_count_top_k_paginated( + path_refs.as_slice(), + self.k, + offset, + self.descending, + transaction, + grove_version, + ); + value + .map_err(|e| Error::GroveDB(Box::new(e)))? + .into_iter() + .map(|(count, key)| RankedEntry { + key, + value: RankedEntryValue::Count(count), + }) + .collect::>() + } + RankedAxis::Sum => { + let CostContext { value, cost: _ } = drive.grove.indexed_sum_top_k_paginated( + path_refs.as_slice(), + self.k, + offset, + self.descending, + transaction, + grove_version, + ); + value + .map_err(|e| Error::GroveDB(Box::new(e)))? + .into_iter() + .map(|(sum, key)| RankedEntry { + key, + value: RankedEntryValue::Sum(sum), + }) + .collect::>() + } + RankedAxis::Avg => { + let CostContext { value, cost: _ } = drive.grove.indexed_avg_top_k_paginated( + path_refs.as_slice(), + self.k, + offset, + self.descending, + transaction, + grove_version, + ); + value + .map_err(|e| Error::GroveDB(Box::new(e)))? + .into_iter() + .map(|(avg, key)| RankedEntry { + key, + value: RankedEntryValue::AvgFixedPoint(avg), + }) + .collect::>() + } + }; + + // `k` is the contract with the caller, and on the prove path it + // is re-checked inside the proof envelope. Asserting it here too + // keeps the no-proof and prove responses shape-identical: a + // caller must never see an over-long list from one path and a + // capped one from the other. + if entries.len() > self.k as usize { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked {:?} read returned {} entries for k = {}", + self.axis, + entries.len(), + self.k + )))); + } + Ok(RankedPage { + skipped: offset, + entries, + }) + } + + /// Generate the grovedb indexed-axis paginated top-k proof for this + /// query. + /// + /// The envelope commits the walked secondary entries, the number of + /// entries skipped to reach them, the primary's root hash, the + /// sibling axes' root hashes, and a per-ancestor attestation chain + /// up to the grovedb root — so the client reconstructs the platform + /// root hash from it. It also echoes `(axis, k, offset, + /// descending)`, which + /// [`grovedb::GroveDb::verify_indexed_axis_top_k_paginated`] + /// re-checks against what the client asked for; that is why `k` is + /// validated rather than clamped upstream (a clamped `k` would + /// produce a proof the client's own reconstruction rejects). + /// + /// The paginated primitive is used unconditionally, with + /// `offset = 0` for offset-free requests, so there is exactly one + /// proof shape on this surface: a client never has to guess which of + /// two envelope formats a server produced. + /// + /// Verified by + /// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof). + /// + /// # Empty rankings prove fine + /// + /// An index holding no documents has an empty axis secondary. The + /// older non-paginated prover refused that outright ("Cannot create + /// proof for empty tree"), which made a freshly registered contract + /// unqueryable with `prove = true`; the paginated prover emits a + /// guaranteed-empty range against the secondary instead, so the + /// proved and unproven paths agree on empty state. Pinned by the + /// `ranking_an_empty_index_reads_empty_and_proves_empty` test. + pub fn execute_top_k_with_proof( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let grove_version = &platform_version.drive.grove_version; + let path = self.indexed_property_name_tree_path()?; + let path_refs: Vec<&[u8]> = path.iter().map(|segment| segment.as_slice()).collect(); + + // Same destructure-don't-unwrap rationale as the no-proof arm. + let CostContext { value, cost: _ } = drive.grove.prove_indexed_axis_top_k_paginated( + path_refs.as_slice(), + self.axis.into(), + self.k, + self.offset as u64, + self.descending, + transaction, + grove_version, + ); + value.map_err(|e| Error::GroveDB(Box::new(e))) + } +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/executors/mod.rs b/packages/rs-drive/src/query/drive_document_ranked_query/executors/mod.rs new file mode 100644 index 00000000000..b48bebc6560 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/executors/mod.rs @@ -0,0 +1,69 @@ +//! Per-mode ranked executors on `impl Drive`. One file per response +//! shape — the dispatcher ([`super::drive_dispatcher`]) picks between +//! them on the request's `prove` flag. +//! +//! Each executor resolves the covering index, builds the +//! [`DriveDocumentRankedQuery`], and runs the matching method on it. The +//! resolution step is shared ([`ranked_query_for_mode`]) rather than +//! duplicated per file: the no-proof and prove paths must agree on the +//! index — and therefore the grove path — or a client would verify a +//! proof about a different subtree than the one an unproven read +//! returned. +//! +//! No re-exports needed: each file adds methods directly to `impl Drive`. + +pub mod top_k_no_proof; +pub mod top_k_proof; + +use super::index_picker::find_ranked_index_for_mode; +use super::{DocumentRankedMode, DriveDocumentRankedQuery}; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use dpp::data_contract::document_type::{DocumentTypeRef, Index}; +use std::collections::BTreeMap; + +/// Resolve a validated [`DocumentRankedMode`] against a document type's +/// indexes into the executable [`DriveDocumentRankedQuery`]. +/// +/// `indexes` is threaded in separately rather than read off +/// `document_type` here because +/// [`DocumentTypeV0Getters::indexes`] borrows its receiver — taking the +/// map from the caller lets the returned query's `&'a Index` outlive this +/// frame. Callers pass `document_type.indexes()`. +/// +/// The only failure is "no index covers this", which is reported with the +/// exact contract keyword the index is missing so the caller can act on +/// it without reading the schema spec. +pub(super) fn ranked_query_for_mode<'a>( + contract_id: [u8; 32], + document_type: DocumentTypeRef<'a>, + document_type_name: String, + indexes: &'a BTreeMap, + mode: &DocumentRankedMode, +) -> Result, Error> { + let index = find_ranked_index_for_mode(indexes, mode).ok_or_else(|| { + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "no ranked index covers `group_by = [{}]` on the {:?} axis: the document type \ + needs a single-property index on `{}` declaring `{}`{}", + mode.group_by_property, + mode.axis, + mode.group_by_property, + mode.axis.required_index_keyword(), + if mode.aggregate_field.is_empty() { + String::new() + } else { + format!(" with `summable: \"{}\"`", mode.aggregate_field) + } + ))) + })?; + Ok(DriveDocumentRankedQuery { + document_type, + contract_id, + document_type_name, + index, + axis: mode.axis, + descending: mode.descending, + k: mode.k, + offset: mode.offset, + }) +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_no_proof.rs b/packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_no_proof.rs new file mode 100644 index 00000000000..1b7b2b9d200 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_no_proof.rs @@ -0,0 +1,37 @@ +//! Ranked executor for `prove = false` — reads one page of `k` groups +//! straight out of the axis secondary, starting at rank `offset`, and +//! returns them in ranking order. + +use super::super::{DocumentRankedMode, RankedPage}; +use super::ranked_query_for_mode; +use crate::drive::Drive; +use crate::error::Error; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// One page of `k` groups on a ranked index, unproven. + /// + /// Entry order is the ranking order; callers must not re-sort. + pub fn execute_document_ranked_top_k_no_proof( + &self, + contract_id: [u8; 32], + document_type: DocumentTypeRef, + document_type_name: String, + mode: &DocumentRankedMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let indexes = document_type.indexes(); + let ranked_query = ranked_query_for_mode( + contract_id, + document_type, + document_type_name, + indexes, + mode, + )?; + ranked_query.execute_top_k_no_proof(self, transaction, platform_version) + } +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_proof.rs b/packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_proof.rs new file mode 100644 index 00000000000..478f498e05d --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_proof.rs @@ -0,0 +1,41 @@ +//! Ranked executor for `prove = true` — generates the grovedb +//! indexed-axis paginated top-k proof envelope for the same +//! `(index, axis, descending, k, offset)` the no-proof executor would +//! read. + +use super::super::DocumentRankedMode; +use super::ranked_query_for_mode; +use crate::drive::Drive; +use crate::error::Error; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Proof of one page of `k` groups on a ranked index. + /// + /// The client verifies it with + /// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof), + /// reconstructing the same query from the same contract — which is + /// why index resolution is shared with the no-proof executor. + pub fn execute_document_ranked_top_k_proof( + &self, + contract_id: [u8; 32], + document_type: DocumentTypeRef, + document_type_name: String, + mode: &DocumentRankedMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let indexes = document_type.indexes(); + let ranked_query = ranked_query_for_mode( + contract_id, + document_type, + document_type_name, + indexes, + mode, + )?; + ranked_query.execute_top_k_with_proof(self, transaction, platform_version) + } +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/index_picker.rs b/packages/rs-drive/src/query/drive_document_ranked_query/index_picker.rs new file mode 100644 index 00000000000..251979e2b87 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/index_picker.rs @@ -0,0 +1,82 @@ +//! Covering-index picker for the ranked query. +//! +//! Pure function on the document type's index map plus the +//! `(group property, axis, aggregate field)` triple +//! [`super::mode_detection`] resolved. No Drive, no proof — the server +//! and the SDK verifier both call it so they land on the same index (and +//! therefore the same grove path) for the same request. + +use super::{DocumentRankedMode, RankedAxis}; +use dpp::data_contract::document_type::Index; +use std::collections::BTreeMap; + +/// Find the index that can serve `axis` ranking grouped by +/// `group_by_property`, aggregating `aggregate_field`. +/// +/// An index qualifies when **all** of: +/// +/// - it has exactly one property, and that property is +/// `group_by_property` — the ranked secondary's group keys are the +/// values of an index's *last* property, and rs-dpp only allows +/// single-property ranked indexes, so "last" and "only" coincide; +/// - it declares the ranking keyword for `axis` +/// ([`RankedAxis::required_index_keyword`]); +/// - for [`RankedAxis::Sum`] / [`RankedAxis::Avg`], its `summable` +/// property is exactly `aggregate_field`. Both axes are derived from +/// the same running sum the index maintains (`Avg` is that sum over the +/// group's count), so summing a *different* field than the one the +/// index accumulates would silently answer about the wrong property. +/// +/// Returns `None` when nothing qualifies; callers turn that into +/// [`crate::error::query::QuerySyntaxError::WhereClauseOnNonIndexedProperty`] +/// with a message naming the missing keyword. +/// +/// At most one index can qualify for a given `(group property, axis, +/// field)` triple — rs-dpp rejects two indexes over the same property set +/// on one document type — so "first match wins" is not a tie-break in +/// practice. Should that ever change, the `BTreeMap` iteration order +/// (index name, ascending) keeps the choice deterministic, which is what +/// prover/verifier agreement actually requires: both sides run this same +/// function over the same contract and must land on the same grove path. +/// +/// Note that axis availability is decided from the index's `ranked_*` +/// flags, **not** from the element variant the write path laid down: a +/// `rankedCountable` index that also declares `rangeSummable` is stored +/// as a `ProvableCountProvableSumIndexedTree` carrying only the Count +/// axis, so the element variant alone would over-report what is rankable. +pub fn find_ranked_index_for_axis<'b>( + indexes: &'b BTreeMap, + group_by_property: &str, + axis: RankedAxis, + aggregate_field: &str, +) -> Option<&'b Index> { + indexes.values().find(|index| { + // Single-property, and that property is the grouping property. + if index.properties.len() != 1 || index.properties[0].name != group_by_property { + return false; + } + match axis { + RankedAxis::Count => index.ranked_countable, + RankedAxis::Sum => { + index.ranked_summable && index.summable.as_deref() == Some(aggregate_field) + } + RankedAxis::Avg => { + index.ranked_averageable && index.summable.as_deref() == Some(aggregate_field) + } + } + }) +} + +/// [`find_ranked_index_for_axis`] driven straight from a resolved +/// [`DocumentRankedMode`] — the shape every caller actually has. +pub fn find_ranked_index_for_mode<'b>( + indexes: &'b BTreeMap, + mode: &DocumentRankedMode, +) -> Option<&'b Index> { + find_ranked_index_for_axis( + indexes, + &mode.group_by_property, + mode.axis, + &mode.aggregate_field, + ) +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/mod.rs b/packages/rs-drive/src/query/drive_document_ranked_query/mod.rs new file mode 100644 index 00000000000..70cc2dd2339 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/mod.rs @@ -0,0 +1,393 @@ +//! Types and module structure for the **ranked** (top-k / bottom-k) +//! document query — +//! `SELECT GROUP BY ORDER BY DESC LIMIT n OFFSET m`. +//! +//! A ranked query answers "which `n` groups score highest (or lowest) on +//! an aggregate, starting from rank `m`?" in `O(log n + k)` with a proof, +//! by reading grovedb's per-axis *secondary* Merk of an indexed tree +//! (grovedb PR #657). The contract opts in per index via +//! `rankedCountable` / `rankedSummable` / `rankedAverageable` (meta +//! schema v3 / PV14); the write path keeps the secondaries in sync. See +//! [`crate::drive::document::ranked_index_tree_type`] for the storage +//! layout this query reads. +//! +//! The implementation is split across siblings, mirroring +//! [`super::drive_document_count_query`]: +//! - [`mode_detection`] — request-shape validation + the versioned +//! [`mode_detection::detect_ranked_mode`] that resolves +//! `(select, group_by, order_by, limit, offset)` into a +//! [`DocumentRankedMode`]. +//! - [`index_picker`] — [`index_picker::find_ranked_index_for_axis`], +//! the covering-index picker for a `(group_by property, axis, +//! aggregate field)` triple. +//! - [`path`] — the load-bearing prover/verifier-agreement path builder +//! ([`DriveDocumentRankedQuery::indexed_property_name_tree_path`]). +//! - [`execute_top_k`] — the two executors on +//! [`DriveDocumentRankedQuery`] (no-proof read, proof generation). +//! - [`executors`] — the `impl Drive` wrappers the dispatcher calls. +//! - [`drive_dispatcher`] — [`DocumentRankedRequest`] / +//! [`DocumentRankedResponse`] and +//! [`crate::drive::Drive::execute_document_ranked_request`]. +//! - [`tests`] (cfg `server` + `test`) — unit + integration tests. +//! +//! ## What makes this query shape different +//! +//! Every other aggregate query in this crate walks *value trees* under a +//! property-name tree and aggregates what it finds. A ranked query never +//! touches the value trees at all: the answer lives pre-sorted in the +//! secondary Merk, keyed by `(sort_key ‖ group_key)`. Three consequences +//! shape the API: +//! +//! 1. **No `where` clauses.** Ranked indexes are single-property only +//! (compound ones are rejected at contract-parse time in rs-dpp — +//! their terminal level is created lazily by the same batch that +//! populates it, which grovedb rejects for indexed trees). With one +//! property there is no equality prefix to narrow, and a `where` on +//! the ranked property itself would ask for a *filtered* ranking, +//! which the secondary cannot express — it is sorted by aggregate, +//! not by group key. Non-empty `where` is therefore rejected rather +//! than silently ignored. +//! 2. **`limit` is mandatory, `offset` is free, `start_at` is refused.** +//! `limit` is the `k` of the walk and the ranked surface has no +//! server default for it, so it must be supplied. `offset` is the +//! rank the page starts at and is unbounded above: grovedb's +//! paginated prover is `O(log n + k)` *regardless of offset* (the +//! skipped region is attested by counted subtree commitments, never +//! walked entry by entry), so a large offset is not a cost lever and +//! needs no ceiling. `start_at` / `start_after` name a document id, +//! which does not appear anywhere in an aggregate-ordered keyspace. +//! 3. **Entry order IS the ranking order.** The executor returns entries +//! in the order grovedb walked the secondary; callers must not +//! re-sort. Ties are broken by group key — see +//! [`DriveDocumentRankedQuery::descending`]. + +#[cfg(any(feature = "server", feature = "verify"))] +use dpp::data_contract::document_type::{DocumentTypeRef, Index}; + +/// The fixed-point scale grovedb's Avg axis sorts by: +/// `avg_fixed_point = floor(sum * RANKED_AVG_SCALE / count)` with +/// euclidean (toward -∞) division. +/// +/// Re-exported from grovedb rather than re-declared so the two can never +/// drift — the encoded sort keys in storage are produced with grovedb's +/// constant, and a platform-side copy that fell out of step would silently +/// mis-scale every average the client renders. +#[cfg(any(feature = "server", feature = "verify"))] +pub use grovedb::element::indexed::AVG_FIXED_POINT_SCALE as RANKED_AVG_SCALE; + +#[cfg(any(feature = "server", feature = "verify"))] +pub mod index_picker; +#[cfg(any(feature = "server", feature = "verify"))] +pub mod mode_detection; +#[cfg(any(feature = "server", feature = "verify"))] +pub mod path; + +// Server-side execution paths. +#[cfg(feature = "server")] +pub mod drive_dispatcher; +#[cfg(feature = "server")] +pub mod execute_top_k; +#[cfg(feature = "server")] +pub mod executors; + +#[cfg(feature = "server")] +pub use drive_dispatcher::{DocumentRankedRequest, DocumentRankedResponse}; + +#[cfg(all(feature = "server", test))] +mod tests; + +/// Hard ceiling on `k` (the request's `LIMIT`). +/// +/// The ranked proof commits one secondary entry per returned group, so +/// proof bytes grow linearly in `k`. 100 keeps the worst case in the same +/// order of magnitude as the other aggregate proof surfaces (compare +/// [`super::conditions::WhereClause::in_values`]'s 100-value cap on `In` +/// fan-out) and matches the `In` bound callers already design against. +/// +/// This is a **hard** ceiling, not a clamp: a request with `limit > 100` +/// is rejected with +/// [`crate::error::query::QuerySyntaxError::InvalidLimit`] rather than +/// silently truncated. Truncation would be especially treacherous here +/// because `k` is echoed inside the proof envelope and re-checked by +/// [`grovedb::GroveDb::verify_indexed_axis_top_k_paginated`] — a +/// server-side clamp would produce a proof the client's own +/// reconstruction rejects. +/// +/// There is deliberately **no companion ceiling on `OFFSET`**; see the +/// module docs and [`DriveDocumentRankedQuery::offset`]. +#[cfg(any(feature = "server", feature = "verify"))] +pub const MAX_RANKED_LIMIT: u16 = 100; + +/// The `ORDER BY` field name that means "the group's `COUNT(*)`". +/// +/// `COUNT(*)` has no field to name, so the ranked grammar needs some +/// token for "order by the thing the select projects". `$count` is +/// chosen because the leading `$` is DPP's **system-property +/// namespace** (`$id`, `$ownerId`, `$revision`, `$createdAt`, …): a +/// document schema cannot declare a property whose name starts with +/// `$`, so the sentinel is guaranteed not to collide with any real +/// property a contract author could write, now or in any future +/// contract. That is the whole reason it is spelled with a sigil rather +/// than as `"count"` — a bare `count` would silently hijack ordering +/// for any schema that happens to have a `count` column. +#[cfg(any(feature = "server", feature = "verify"))] +pub const RANKED_COUNT_ORDER_KEY: &str = "$count"; + +/// Which per-group aggregate the groups are ranked by. +/// +/// Maps 1:1 onto [`grovedb::element::IndexAxis`], the axis tag stored in +/// an indexed tree's TLV and echoed in the proof envelope. Kept as a +/// separate drive-side type (rather than re-exporting grovedb's) so the +/// query surface's error messages and validation can talk about +/// `rankedCountable` / `rankedSummable` / `rankedAverageable` — contract +/// grammar the storage layer knows nothing about. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(any(feature = "server", feature = "verify"))] +pub enum RankedAxis { + /// Rank by the number of documents in each group. Requires the + /// index to declare `rankedCountable`. + Count, + /// Rank by the running sum of the index's `summable` property across + /// each group. Requires `rankedSummable`. + Sum, + /// Rank by each group's average of the index's `summable` property, + /// as the fixed-point value described on [`RANKED_AVG_SCALE`]. + /// Requires `rankedAverageable`. + Avg, +} + +#[cfg(any(feature = "server", feature = "verify"))] +impl From for grovedb::element::IndexAxis { + fn from(axis: RankedAxis) -> Self { + match axis { + RankedAxis::Count => grovedb::element::IndexAxis::Count, + RankedAxis::Sum => grovedb::element::IndexAxis::Sum, + RankedAxis::Avg => grovedb::element::IndexAxis::Avg, + } + } +} + +#[cfg(any(feature = "server", feature = "verify"))] +impl RankedAxis { + /// The contract-grammar keyword an index must declare to be rankable + /// on this axis. Used in error messages so a rejected query names the + /// exact schema key the contract author has to add. + pub fn required_index_keyword(self) -> &'static str { + match self { + RankedAxis::Count => "rankedCountable", + RankedAxis::Sum => "rankedSummable", + RankedAxis::Avg => "rankedAverageable", + } + } +} + +/// The aggregate value carried by one ranked entry. Mirrors grovedb's +/// [`grovedb::operations::proof::indexed_axis::AxisEntries`] variants +/// exactly, one scalar at a time, so a `Vec` and an +/// `AxisEntries` carry the same information with the same types. +/// +/// The variant is redundant with the request's [`RankedAxis`] by +/// construction; carrying it per entry means a decoded response is +/// self-describing (no need to thread the request alongside it to know +/// how to interpret the number), and lets both the executor and the +/// verifier fail loudly if grovedb ever hands back an axis's entries +/// under a different axis's request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(any(feature = "server", feature = "verify"))] +pub enum RankedEntryValue { + /// Document count in the group ([`RankedAxis::Count`]). + Count(u64), + /// Running sum over the group ([`RankedAxis::Sum`]). + Sum(i64), + /// Fixed-point average over the group ([`RankedAxis::Avg`]); + /// divide by [`RANKED_AVG_SCALE`] for the real value, or use + /// [`Self::as_f64`]. + AvgFixedPoint(i128), +} + +#[cfg(any(feature = "server", feature = "verify"))] +impl RankedEntryValue { + /// The axis this value came from. + pub fn axis(self) -> RankedAxis { + match self { + RankedEntryValue::Count(_) => RankedAxis::Count, + RankedEntryValue::Sum(_) => RankedAxis::Sum, + RankedEntryValue::AvgFixedPoint(_) => RankedAxis::Avg, + } + } + + /// The value as an `f64`, with the Avg variant scaled down by + /// [`RANKED_AVG_SCALE`]. + /// + /// Lossy for large counts / sums (beyond 2^53) and for averages — + /// this is a display helper. Consensus-relevant comparisons must use + /// the exact integer variants; two groups whose fixed-point averages + /// differ can round to the same `f64`. + pub fn as_f64(self) -> f64 { + match self { + RankedEntryValue::Count(count) => count as f64, + RankedEntryValue::Sum(sum) => sum as f64, + RankedEntryValue::AvgFixedPoint(avg) => (avg as f64) / (RANKED_AVG_SCALE as f64), + } + } +} + +/// One group in a ranked result: the group's index key plus its aggregate. +/// +/// `key` is the **raw index-key bytes of the grouping property's value** — +/// the same bytes that name the group's value tree under the indexed +/// property-name tree (for a `string` property, its UTF-8 bytes). Callers +/// that want the original typed value decode it with the document type's +/// key deserialization; the query layer deliberately hands back bytes so +/// prover and verifier agree without a DPP round-trip. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(any(feature = "server", feature = "verify"))] +pub struct RankedEntry { + /// Raw index-key bytes of the grouped property value. + pub key: Vec, + /// The group's aggregate on the requested axis. + pub value: RankedEntryValue, +} + +/// A resolved ranked query. Shared by the prover and the verifier — both +/// build the grove path through +/// [`DriveDocumentRankedQuery::indexed_property_name_tree_path`], so the +/// two cannot drift on which subtree the proof is about. +/// +/// Construction is normally left to +/// [`crate::drive::Drive::execute_document_ranked_request`] (server) or to +/// the SDK's proof helpers (client); both go through +/// [`index_picker::find_ranked_index_for_axis`] to resolve `index`. +#[derive(Debug, Clone)] +#[cfg(any(feature = "server", feature = "verify"))] +pub struct DriveDocumentRankedQuery<'a> { + /// The document type being ranked. + pub document_type: DocumentTypeRef<'a>, + /// The contract id (32 bytes). Separate from `document_type` so the + /// verifier can build the query without the full contract. + pub contract_id: [u8; 32], + /// The document type name — a path segment. + pub document_type_name: String, + /// The covering ranked index. Single-property by construction; its + /// one property is both the `GROUP BY` property and the last path + /// segment. + pub index: &'a Index, + /// Which aggregate the groups are ranked by. Must be covered by + /// `index`'s matching `ranked_*` flag. + pub axis: RankedAxis, + /// `true` walks the secondary from the largest aggregate down + /// (`ORDER BY DESC`); `false` walks from the smallest up + /// (`ORDER BY ASC`). + /// + /// **Tie ordering.** The secondary's keys are `(sort_key ‖ + /// group_key)`, and the walk is a plain directional scan of that + /// keyspace — so groups with equal aggregates come back in group-key + /// order *in the direction of the walk*: ascending group key when + /// `descending == false`, and **descending group key when + /// `descending == true`**. The reversal is a property of the scan, + /// not a separate tie-break rule; it is pinned by the + /// `ties_break_by_group_key_in_the_walk_direction` test. + pub descending: bool, + /// How many groups to return — the request's `LIMIT`. + /// `1 ..= MAX_RANKED_LIMIT`, validated in [`mode_detection`]. Fewer + /// entries come back when the index has fewer groups than + /// `offset + k`; that is not an error. + pub k: u16, + /// How many ranks to skip before the returned page — the request's + /// `OFFSET`. `0` for an unpaginated ranking. + /// + /// Unbounded above (any `u32`), on purpose. grovedb attests the + /// skipped region through the counted subtree commitments + /// (`HashWithCount` / `HashWithCountAndSum`) rather than by walking + /// it, so both the prover's work and the proof's size stay + /// `O(log n + k)` **at any offset** — an offset of 4 and an offset + /// of four billion cost the same. There is therefore no + /// denial-of-service lever to cap, and capping would only stop + /// honest deep pagination. + /// + /// An offset past the end of the secondary is a provable answer, not + /// an error: the page comes back empty and + /// [`RankedPage::skipped`] is the secondary's entire population. + pub offset: u32, +} + +/// A page of a ranked result: the entries, plus how many ranks were +/// actually skipped to reach them. +/// +/// `skipped` is what turns a page into a *ranking*: entry `i` of +/// `entries` is the group at rank `skipped + i` (0-based). Without it a +/// caller that asked for `OFFSET 4 LIMIT 1` would receive one entry and +/// have to trust the server that it really is the 5th-best group. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(any(feature = "server", feature = "verify"))] +pub struct RankedPage { + /// Number of secondary entries skipped before this page. + /// + /// On the **proved** path this is grovedb's cryptographically + /// attested count, independently re-derived by the verifier from the + /// counted subtree commitments in the proof bytes: it equals the + /// requested offset unless the walk ran out of entries first, in + /// which case `entries` is empty and `skipped` is a proof that the + /// secondary holds exactly `skipped` groups in total. + /// + /// On the **unproven** read there is nothing to attest and grovedb's + /// read API does not report the short walk, so this is simply the + /// requested offset. The two paths therefore disagree in exactly one + /// case — an offset past the end — where the unproven read reports + /// the requested offset and the proved one reports the true + /// population. Callers that need the population must prove. + pub skipped: u64, + /// The groups on this page, **in ranking order**. Never longer than + /// the query's `k`. + pub entries: Vec, +} + +/// The pagination knobs a ranked request carries, bundled so the +/// versioned validator reads them in one place. +/// +/// `limit` is required (it is the ranking's `k`), `offset` is optional +/// and defaults to `0`, and `start_at` is refused outright — a cursor +/// names a document id, and document ids do not appear in a keyspace +/// sorted by aggregate. `has_start_at` is a bare `bool` because the +/// value is never used; only its presence is an error. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[cfg(any(feature = "server", feature = "verify"))] +pub struct RankedPaginationInputs { + /// The request's `limit`. Required in ranked mode. + pub limit: Option, + /// The request's `offset`, if it set one. `None` means rank 0. + pub offset: Option, + /// Whether the request carried a `start_at` / `start_after` cursor. + pub has_start_at: bool, +} + +/// The resolved shape of a ranked request: which axis, which direction, +/// how many groups, and the `(group property, aggregate field)` pair the +/// index picker needs. +/// +/// Produced by [`mode_detection::detect_ranked_mode`] from the caller's +/// `(select, group_by, order_by, limit, offset)` inputs. Parallels +/// [`super::drive_document_count_query::DocumentCountMode`] in role — +/// the versioned classification of a request — but carries data rather +/// than being a bare discriminant, because the ranked surface has exactly +/// one executor pair (no-proof / proof) and all of its variation is in +/// these five values. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(any(feature = "server", feature = "verify"))] +pub struct DocumentRankedMode { + /// The ranking axis, from the `SELECT` function. + pub axis: RankedAxis, + /// Walk direction: `ORDER BY … DESC` ⇒ `true`, `ASC` ⇒ `false`. + pub descending: bool, + /// Number of groups requested — the `LIMIT`, `1 ..= MAX_RANKED_LIMIT`. + pub k: u16, + /// Ranks to skip — the `OFFSET`, `0` when unset. + pub offset: u32, + /// The single `GROUP BY` property; must be the ranked index's only + /// property. + pub group_by_property: String, + /// The field the aggregate applies to. Empty for + /// [`RankedAxis::Count`] (`COUNT(*)`); the index's `summable` + /// property for [`RankedAxis::Sum`] / [`RankedAxis::Avg`]. + pub aggregate_field: String, +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/mode_detection.rs b/packages/rs-drive/src/query/drive_document_ranked_query/mode_detection.rs new file mode 100644 index 00000000000..f42de99cfe8 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/mode_detection.rs @@ -0,0 +1,335 @@ +//! Request-shape validation for the ranked query, and the versioned +//! `(select, group_by, order_by, limit, offset)` → [`DocumentRankedMode`] +//! resolution. +//! +//! Pure functions on the request shape — no Drive, no contract, no +//! indexes. Available under `server` (the dispatcher validates before +//! executing) and `verify` (the SDK validates the same way before +//! attempting proof verification), so both sides agree on which requests +//! are well-formed and on the `(axis, descending, k, offset)` tuple a +//! well-formed one resolves to. Index-dependent validation ("does an +//! index actually cover this axis?") needs the document type's index map +//! and lives in [`super::index_picker`]. +//! +//! Versioned through +//! `platform_version.drive.methods.document.query.detect_ranked_mode`, +//! the same way +//! [`DriveDocumentCountQuery::detect_mode_versioned`](super::super::drive_document_count_query::DriveDocumentCountQuery::detect_mode_versioned) +//! routes count's table: the accepted request grammar is a consensus- +//! adjacent contract on the query surface, so relaxing it later has to +//! land behind a method-version bump rather than changing what an +//! already-deployed protocol version accepts. + +use super::{ + DocumentRankedMode, RankedAxis, RankedPaginationInputs, MAX_RANKED_LIMIT, + RANKED_COUNT_ORDER_KEY, +}; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::having::HavingClause; +use crate::query::projection::{SelectFunction, SelectProjection}; +use crate::query::{OrderClause, WhereClause}; +use dpp::version::PlatformVersion; + +/// Versioned entry point. Routes through +/// `platform_version.drive.methods.document.query.detect_ranked_mode`; +/// today only `0` is defined and maps to [`detect_ranked_mode_v0`] +/// verbatim. +pub fn detect_ranked_mode( + select: &SelectProjection, + group_by: &[String], + having: &[HavingClause], + order_by: &[OrderClause], + where_clauses: &[WhereClause], + pagination: RankedPaginationInputs, + platform_version: &PlatformVersion, +) -> Result { + match platform_version + .drive + .methods + .document + .query + .detect_ranked_mode + { + 0 => detect_ranked_mode_v0( + select, + group_by, + having, + order_by, + where_clauses, + pagination, + ), + version => Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "detect_ranked_mode: unknown method version {version}; only 0 is supported" + )))), + } +} + +/// The `ORDER BY` field name that names a given select's aggregate. +/// +/// `SUM(f)` / `AVG(f)` are ordered by naming `f` — the same field the +/// projection aggregates, which is how SQL's `ORDER BY avg(grade)` +/// reads once the aggregate function is already fixed by the `SELECT`. +/// `COUNT(*)` has no field, so it is named by the +/// [`RANKED_COUNT_ORDER_KEY`] sentinel. +/// +/// Public because request *builders* need it as much as the validator +/// does: an SDK offering `.order_by_selected_aggregate(…)` has to emit +/// the same string this function expects to read back, and a second +/// copy of the sentinel rule is a silent-rejection bug waiting for the +/// first `COUNT(*)` ranking. +pub fn ranked_order_key(select: &SelectProjection) -> &str { + match select.function { + SelectFunction::Count if select.field.is_empty() => RANKED_COUNT_ORDER_KEY, + _ => select.field.as_str(), + } +} + +/// v0 of the ranked request grammar. +/// +/// Accepts exactly: +/// +/// ```text +/// SELECT COUNT(*) GROUP BY p ORDER BY $count [ASC|DESC] LIMIT n [OFFSET m] +/// SELECT SUM(f) GROUP BY p ORDER BY f [ASC|DESC] LIMIT n [OFFSET m] +/// SELECT AVG(f) GROUP BY p ORDER BY f [ASC|DESC] LIMIT n [OFFSET m] +/// ``` +/// +/// with no `WHERE`, no `HAVING`, no `START AT` / `START AFTER`, exactly +/// one `GROUP BY` property, exactly one `ORDER BY` clause naming the +/// selected aggregate, `1 ≤ n ≤` [`MAX_RANKED_LIMIT`], and any +/// `m ≥ 0`. +/// +/// `DESC` walks the axis from the largest aggregate down (the "top n" +/// reading), `ASC` from the smallest up (the "bottom n" reading). +/// +/// Worked examples: +/// +/// ```text +/// -- the three best restaurants by average grade +/// SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 3 +/// +/// -- the single worst restaurant by average grade +/// SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade ASC LIMIT 1 +/// +/// -- the *5th* best grade: skip the four above it, take one +/// SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 1 OFFSET 4 +/// +/// -- the busiest ten restaurants, second page +/// SELECT COUNT(*) GROUP BY restaurantId ORDER BY $count DESC LIMIT 10 OFFSET 10 +/// ``` +/// +/// Everything the grammar rejects is rejected *loudly* rather than +/// normalized away: a caller who wrote a filter, a `having`, or an +/// ordering on a property the select does not aggregate asked for +/// something this executor cannot deliver, and silently answering a +/// different question is worse than an error. +pub fn detect_ranked_mode_v0( + select: &SelectProjection, + group_by: &[String], + having: &[HavingClause], + order_by: &[OrderClause], + where_clauses: &[WhereClause], + pagination: RankedPaginationInputs, +) -> Result { + // ---- GROUP BY: exactly one property ---------------------------- + // + // Ranked indexes are single-property (rs-dpp rejects compound ones + // at parse time), and the sole property is what the secondary's + // group keys are. Zero group_by would ask to rank a single global + // aggregate against itself; two or more would need a compound index. + if group_by.len() != 1 { + return Err(Error::Query(QuerySyntaxError::InvalidParameter(format!( + "ranked queries require exactly one `group_by` property (the ranked index's \ + only property); got {}. Ranked indexes are single-property — compound ranked \ + indexes are rejected at contract-parse time — so there is no compound \ + grouping to rank over.", + group_by.len() + )))); + } + let group_by_property = group_by[0].clone(); + if group_by_property.is_empty() { + return Err(Error::Query(QuerySyntaxError::InvalidParameter( + "ranked queries require a non-empty `group_by` property name".to_string(), + ))); + } + + // ---- SELECT: the axis, and the field it aggregates -------------- + let (axis, aggregate_field) = match (select.function, select.field.as_str()) { + (SelectFunction::Count, "") => (RankedAxis::Count, String::new()), + (SelectFunction::Count, field) => { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "ranked queries support `COUNT(*)` but not `COUNT({field})`; a ranked \ + count axis counts documents per group, which is what `COUNT(*)` means. \ + Drop the field to rank by group size." + )))); + } + (SelectFunction::Sum, "") | (SelectFunction::Avg, "") => { + return Err(Error::Query(QuerySyntaxError::InvalidParameter( + "`SUM` / `AVG` ranked queries require a non-empty select field naming the \ + index's `summable` property" + .to_string(), + ))); + } + (SelectFunction::Sum, field) => (RankedAxis::Sum, field.to_string()), + (SelectFunction::Avg, field) => (RankedAxis::Avg, field.to_string()), + (other, _) => { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "ranked queries support `COUNT(*)`, `SUM(field)` and `AVG(field)` \ + selects; got {other:?}. Ranking is driven by an indexed tree's \ + per-axis secondary, and grovedb maintains exactly those three axes." + )))); + } + }; + + // ---- ORDER BY: exactly one clause, naming the selected aggregate - + // + // This clause *is* the ranking. One clause, because the secondary + // is a single-key ordering: a second sort key would need a second + // axis the storage does not maintain (ties are already resolved, + // by group key, as a property of the directional scan). + let expected_order_key = ranked_order_key(select); + let order_clause = match order_by { + [only] => only, + _ => { + return Err(Error::Query(QuerySyntaxError::InvalidParameter(format!( + "ranked queries require exactly one `order_by` clause — the one that names \ + the selected aggregate (`ORDER BY {expected_order_key}`); got {}. The axis \ + secondary is a single-key ordering, so there is no second sort key to \ + apply; ties are broken by group key in the direction of the walk.", + order_by.len() + )))); + } + }; + if order_clause.field != expected_order_key { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "`GROUP BY {group_by_property} ORDER BY {}` is not supported: with a `GROUP BY` \ + present, the only ordering the ranked storage can serve is one on the selected \ + aggregate itself — write `ORDER BY {expected_order_key}` for \ + `SELECT {:?}({})`{}. Ordering groups by anything else (a raw document property, \ + or a second aggregate) would need a sort the axis secondary does not maintain.", + order_clause.field, + select.function, + if select.field.is_empty() { + "*" + } else { + select.field.as_str() + }, + if axis == RankedAxis::Count { + format!( + " (`{RANKED_COUNT_ORDER_KEY}` is the sentinel for COUNT(*), which has no \ + field of its own; the `$` prefix is DPP's system namespace so it cannot \ + collide with a schema property)" + ) + } else { + String::new() + } + )))); + } + // `ORDER BY DESC` is the "highest first" reading, so + // descending is the negation of the clause's ascending flag. + let descending = !order_clause.ascending; + + // ---- HAVING: must be absent ------------------------------------- + // + // `having` is a boolean per-group predicate. Composing one with an + // aggregate ordering means "rank only the groups that pass the + // filter", and the axis secondary cannot do that: it is ordered by + // aggregate, and there is no way to skip non-matching groups + // without walking (and proving) every one of them, which destroys + // the O(log n + k) bound the surface exists for. + if !having.is_empty() { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "boolean `having` cannot yet be combined with an aggregate `order_by`: got {} \ + having clause(s) alongside `ORDER BY {expected_order_key}`. Ranking reads a \ + pre-sorted axis secondary; filtering groups first would require walking every \ + group to test the predicate, which is exactly the cost the ranked surface \ + exists to avoid. Drop the `having`, or use a non-ranked grouped aggregate.", + having.len() + )))); + } + + // ---- WHERE: must be absent -------------------------------------- + // + // Rejected rather than ignored. A single-property ranked index has no + // equality prefix a clause could narrow, and a clause on the ranked + // property itself asks for a ranking over a filtered subset — which + // the secondary cannot answer, because it is ordered by aggregate, + // not by group key. Silently dropping the filter would return the + // global ranking under the guise of a filtered one. + if !where_clauses.is_empty() { + return Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "ranked queries do not accept `where` clauses: ranked indexes are \ + single-property, so there is no equality prefix to narrow, and the axis \ + secondary is ordered by aggregate rather than by group key — it cannot \ + rank a filtered subset. Rank the whole index, or add a narrower index.", + ), + )); + } + + // ---- LIMIT: required, 1 ..= MAX_RANKED_LIMIT --------------------- + // + // Required rather than defaulted: `k` is echoed inside the proof + // envelope and re-checked by the verifier, so a server-chosen + // default would be a number the client never agreed to and could + // not reproduce when rebuilding the query to verify. + let limit = pagination.limit.ok_or_else(|| { + Error::Query(QuerySyntaxError::InvalidLimit(format!( + "ranked queries require an explicit `limit` (1 ..= {MAX_RANKED_LIMIT}): it is \ + the number of groups the walk returns, and it is echoed in the proof envelope \ + and re-checked by the verifier, so there is no server-side default a client \ + could reproduce. Write `ORDER BY {expected_order_key} DESC LIMIT 1` for the \ + single best-ranked group." + ))) + })?; + if limit == 0 { + return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!( + "`LIMIT 0` selects nothing; ranked queries require 1 ≤ limit ≤ {MAX_RANKED_LIMIT}" + )))); + } + if limit > MAX_RANKED_LIMIT as u32 { + return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!( + "`LIMIT {limit}` exceeds the ranked-query ceiling of {MAX_RANKED_LIMIT}; the \ + proof commits one secondary entry per returned group, so its size grows \ + linearly in the limit. Narrow the request — the ceiling is a hard limit, not \ + a clamp, because `k` is echoed in the proof envelope and re-checked by the \ + verifier. Deep results are reached with `OFFSET`, which costs nothing." + )))); + } + // Bounded by MAX_RANKED_LIMIT (a u16) immediately above. + let k = limit as u16; + + // ---- OFFSET: optional, unbounded -------------------------------- + // + // No ceiling, and that is a deliberate statement about cost rather + // than an oversight: grovedb's paginated prover attests the skipped + // region from the counted subtree commitments instead of walking + // it, so proving `OFFSET 4` and `OFFSET 4_000_000_000` are the same + // O(log n + k) work and the same proof size. There is no + // denial-of-service lever here to cap, and an arbitrary cap would + // only break honest deep pagination. An offset past the end is a + // provable answer (empty page, `skipped` attesting the population), + // not an error. + let offset = pagination.offset.unwrap_or(0); + + // ---- START AT: must be absent ----------------------------------- + if pagination.has_start_at { + return Err(Error::Query(QuerySyntaxError::InvalidLimit( + "ranked queries do not accept `start_at` / `start_after`: the cursor names a \ + document id, but a ranked walk iterates an aggregate-ordered keyspace in \ + which document ids do not appear. Paginate with `OFFSET` instead — it is \ + O(log n + k) at any depth." + .to_string(), + ))); + } + + Ok(DocumentRankedMode { + axis, + descending, + k, + offset, + group_by_property, + aggregate_field, + }) +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/path.rs b/packages/rs-drive/src/query/drive_document_ranked_query/path.rs new file mode 100644 index 00000000000..be504f18ea2 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/path.rs @@ -0,0 +1,64 @@ +//! The grove path a ranked read / proof / verification is issued against. +//! +//! This is the **prover/verifier-agreement boundary** for the ranked +//! surface. Unlike the count and sum surfaces there is no `PathQuery` to +//! agree on — grovedb's indexed-axis envelope carries the query shape +//! itself and re-checks `(axis, k, descending)` at verification time — so +//! the entire shared contract is these path segments. Prover and verifier +//! both call [`DriveDocumentRankedQuery::indexed_property_name_tree_path`]; +//! a divergence here surfaces as a failed root-hash reconstruction, not a +//! wrong answer, but it is still the one place the two sides must not +//! drift. +//! +//! Gated `any(server, verify)` so the verifier crate reaches it through +//! `DriveDocumentRankedQuery::*` method syntax. + +use super::DriveDocumentRankedQuery; +use crate::drive::RootTree; +use crate::error::drive::DriveError; +use crate::error::Error; + +impl DriveDocumentRankedQuery<'_> { + /// Path of the **terminal property-name tree** — the indexed tree + /// whose primary holds one value tree per group and whose per-axis + /// secondaries hold the ranking. + /// + /// ```text + /// [ RootTree::DataContractDocuments as u8 ] // 0x01 + /// / + /// / [ 0x01 ] // "documents", not "contract" + /// / + /// / + /// ``` + /// + /// The children of that tree are the groups, keyed by the raw + /// index-key bytes of the property value — the same bytes that come + /// back as [`super::RankedEntry::key`]. + /// + /// Errors when the index is not single-property. A compound ranked + /// index would terminate one level *below* a prefix value tree, so + /// its path would need the prefix property's value — which only a + /// `where` clause could supply, and ranked queries take none. rs-dpp + /// rejects compound ranked indexes at contract-parse time, so this is + /// a fail-closed backstop for the day that grammar relaxes: better a + /// typed error than a path pointing at a prefix level whose element + /// is not an indexed tree at all. + pub fn indexed_property_name_tree_path(&self) -> Result>, Error> { + let [property] = self.index.properties.as_slice() else { + return Err(Error::Drive(DriveError::NotSupported( + "ranked queries require a single-property index: the ranked secondary \ + lives on the index's terminal property-name tree, and for a compound \ + index that tree sits under a prefix value tree whose value only a \ + `where` clause could name — but ranked queries accept no `where` \ + clauses", + ))); + }; + Ok(vec![ + vec![RootTree::DataContractDocuments as u8], + self.contract_id.to_vec(), + vec![1u8], + self.document_type_name.as_bytes().to_vec(), + property.name.as_bytes().to_vec(), + ]) + } +} diff --git a/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs b/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs new file mode 100644 index 00000000000..07bc14ccb23 --- /dev/null +++ b/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs @@ -0,0 +1,2035 @@ +//! Tests for the ranked query surface. +//! +//! Three layers, in that order in this file: +//! +//! 1. **Grammar** — [`mode_detection`](super::mode_detection) and +//! [`index_picker`](super::index_picker) are pure functions, so the +//! accept/reject matrix is exercised without a Drive. +//! 2. **Behaviour** — the dispatcher run end to end against the +//! `restaurants` fixture, on all three axes, no-proof and proved, with +//! the proof round-tripped through +//! [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`] and checked +//! against the live grovedb root hash. +//! 3. **Index picking under competition** — the same round trip against +//! an inline contract whose single doctype carries two ranked indexes +//! on different properties and axes plus a non-ranked compound +//! sibling, so that *which* index is chosen becomes observable and the +//! prover and the client have something to disagree about. +//! +//! The fixture (`tests/supporting_files/contract/restaurants/`) is shared +//! with the write-path e2e suite and is read-only here. Its three +//! doctypes give one doctype per ranking axis, because two indexes over +//! the same property set on one doctype is a `DuplicateIndexError`: +//! +//! | doctype | index | axis | aggregated property | +//! |--------------|---------------------------|-------|---------------------| +//! | `review` | `byRestaurant` | Avg | `grade` | +//! | `visit` | `byRestaurantVisits` | Count | — (`COUNT(*)`) | +//! | `tip` | `byRestaurantTips` | Sum | `amount` | +//! | `adjustment` | `byRestaurantAdjustments` | Avg | `delta` (signed) | +//! +//! `adjustment` exists for the write-path suite's signed-average coverage +//! (`delta` admits negatives, `grade` does not) and is unused here. + +use super::index_picker::find_ranked_index_for_axis; +use super::mode_detection::{detect_ranked_mode, detect_ranked_mode_v0}; +use super::*; +use crate::drive::Drive; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::having::{ + HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, +}; +use crate::query::projection::{SelectFunction, SelectProjection}; +use crate::query::{OrderClause, WhereClause, WhereOperator}; +use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; +use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; +use crate::util::storage_flags::StorageFlags; +use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::random_document::CreateRandomDocument; +use dpp::data_contract::document_type::{Index, IndexCountability, IndexProperty}; +use dpp::document::{Document, DocumentV0Setters}; +use dpp::platform_value::Value; +use dpp::prelude::DataContract; +use dpp::tests::json_document::json_document_to_contract; +use dpp::version::PlatformVersion; +use grovedb::element::indexed::compute_avg_fixed_point; +use std::collections::BTreeMap; + +/// The one property every fixture doctype groups by. +const GROUP_PROPERTY: &str = "restaurantId"; + +fn platform_version() -> &'static PlatformVersion { + PlatformVersion::latest() +} + +// =================================================================== +// Grammar: mode detection +// =================================================================== + +fn group_by() -> Vec { + vec![GROUP_PROPERTY.to_string()] +} + +/// The single `ORDER BY [ASC|DESC]` clause that carries the +/// ranking. `ascending = false` is the `DESC` / "highest first" +/// reading. +fn order_by(field: &str, ascending: bool) -> Vec { + vec![OrderClause { + field: field.to_string(), + ascending, + }] +} + +/// `LIMIT limit [OFFSET offset]`, with no cursor. +fn page(limit: Option, offset: Option) -> RankedPaginationInputs { + RankedPaginationInputs { + limit, + offset, + has_start_at: false, + } +} + +/// `SELECT AVG(grade) … GROUP BY restaurantId ORDER BY grade …` — the +/// fixture's headline shape, parameterized on direction and pagination +/// so the mapping tests can sweep them. +fn detect_avg( + ascending: bool, + limit: Option, + offset: Option, +) -> Result { + detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by("grade", ascending), + &[], + page(limit, offset), + ) +} + +/// The `ORDER BY` direction is the walk direction and the `LIMIT` is +/// `k`, exactly as the SQL reading demands: `DESC` is "highest first", +/// `ASC` is "lowest first", and `LIMIT 1` is how the single +/// best-ranked group is asked for. +#[test] +fn order_direction_maps_to_walk_direction_and_limit_is_k() { + let desc = detect_avg(false, Some(5), None).expect("ORDER BY … DESC LIMIT 5 is well-formed"); + assert!(desc.descending, "DESC ranks highest-first"); + assert_eq!(desc.k, 5); + + let asc = detect_avg(true, Some(3), None).expect("ORDER BY … ASC LIMIT 3 is well-formed"); + assert!(!asc.descending, "ASC ranks lowest-first"); + assert_eq!(asc.k, 3); + + let desc_one = detect_avg(false, Some(1), None).expect("LIMIT 1 is well-formed"); + assert!(desc_one.descending); + assert_eq!( + desc_one.k, 1, + "DESC LIMIT 1 is the single best-ranked group" + ); + + let asc_one = detect_avg(true, Some(1), None).expect("LIMIT 1 is well-formed"); + assert!(!asc_one.descending); + assert_eq!(asc_one.k, 1, "ASC LIMIT 1 is the single worst-ranked group"); +} + +/// `OFFSET` is optional and defaults to rank 0; when present it is +/// carried through verbatim, with no ceiling of its own. The ceiling is +/// deliberately absent because grovedb's paginated proof is +/// `O(log n + k)` at any offset — the skipped region is attested by +/// counted subtree commitments rather than walked — so a large offset +/// is not a cost lever. +#[test] +fn offset_is_optional_defaults_to_zero_and_is_uncapped() { + assert_eq!( + detect_avg(false, Some(3), None) + .expect("no OFFSET is well-formed") + .offset, + 0, + "an absent OFFSET means the page starts at rank 0" + ); + + // "the 5th best grade" — skip the four above it, take one. + let fifth_best = detect_avg(false, Some(1), Some(4)).expect("LIMIT 1 OFFSET 4 is well-formed"); + assert_eq!(fifth_best.k, 1); + assert_eq!(fifth_best.offset, 4); + assert!(fifth_best.descending); + + // Far past any plausible population, and far past MAX_RANKED_LIMIT: + // still accepted, because offset costs nothing to prove. + let deep = detect_avg(false, Some(10), Some(u32::MAX)).expect("a huge OFFSET is well-formed"); + assert_eq!(deep.offset, u32::MAX); + assert_eq!(deep.k, 10); +} + +/// The resolved mode carries everything the index picker needs, not just +/// the ranking: which axis, which property to group on, and which field +/// the aggregate applies to. +#[test] +fn resolved_mode_carries_axis_group_property_and_field() { + let avg = detect_avg(false, Some(2), None).expect("well-formed"); + assert_eq!(avg.axis, RankedAxis::Avg); + assert_eq!(avg.group_by_property, GROUP_PROPERTY); + assert_eq!(avg.aggregate_field, "grade"); + + let count = detect_ranked_mode_v0( + &SelectProjection::count_star(), + &group_by(), + &[], + &order_by(RANKED_COUNT_ORDER_KEY, false), + &[], + page(Some(2), None), + ) + .expect("COUNT(*) is well-formed"); + assert_eq!(count.axis, RankedAxis::Count); + assert_eq!( + count.aggregate_field, "", + "COUNT(*) has no field — the count is of documents, not of values" + ); + + let sum = detect_ranked_mode_v0( + &SelectProjection::sum("amount"), + &group_by(), + &[], + &order_by("amount", true), + &[], + page(Some(1), None), + ) + .expect("SUM is well-formed"); + assert_eq!(sum.axis, RankedAxis::Sum); + assert_eq!(sum.aggregate_field, "amount"); +} + +/// `COUNT(*)` is ordered by the `$count` sentinel and by nothing else. +/// +/// The sentinel exists because `COUNT(*)` has no field to name. `$` is +/// DPP's system-property namespace, which a document schema cannot use, +/// so the token is collision-proof against every contract that could +/// ever be written — a bare `"count"` would hijack ordering for any +/// schema that happens to have a `count` column, which is exactly the +/// failure this test guards. +#[test] +fn count_star_is_ordered_by_the_dollar_count_sentinel() { + assert!( + RANKED_COUNT_ORDER_KEY.starts_with('$'), + "the sentinel must live in the system-property namespace so it cannot collide \ + with a schema property" + ); + + let mode = detect_ranked_mode_v0( + &SelectProjection::count_star(), + &group_by(), + &[], + &order_by(RANKED_COUNT_ORDER_KEY, false), + &[], + page(Some(4), None), + ) + .expect("`ORDER BY $count DESC LIMIT 4` is the COUNT(*) ranking"); + assert_eq!(mode.axis, RankedAxis::Count); + assert_eq!(mode.k, 4); + + // A plain `count` column name is *not* the sentinel, even though a + // reader might expect it to be. + let error = detect_ranked_mode_v0( + &SelectProjection::count_star(), + &group_by(), + &[], + &order_by("count", false), + &[], + page(Some(4), None), + ) + .expect_err("only the `$`-prefixed sentinel names the COUNT(*) axis"); + match error { + Error::Query(QuerySyntaxError::Unsupported(message)) => { + assert!( + message.contains(RANKED_COUNT_ORDER_KEY), + "the rejection must name the sentinel to write instead, got: {message}" + ); + } + other => panic!("expected an Unsupported query error, got {other}"), + } +} + +/// `LIMIT 0` selects nothing and `LIMIT > MAX_RANKED_LIMIT` is refused +/// rather than clamped — a clamp would produce a proof whose echoed `k` +/// the client's own reconstruction rejects. The boundary itself is +/// accepted. +#[test] +fn k_is_bounded_to_one_through_max_ranked_limit() { + let zero = detect_avg(false, Some(0), None).expect_err("LIMIT 0 must be rejected"); + assert!(matches!( + zero, + Error::Query(QuerySyntaxError::InvalidLimit(_)) + )); + + let over = detect_avg(false, Some(MAX_RANKED_LIMIT as u32 + 1), None) + .expect_err("LIMIT 101 must be rejected"); + assert!(matches!( + over, + Error::Query(QuerySyntaxError::InvalidLimit(_)) + )); + + let at_limit = detect_avg(false, Some(MAX_RANKED_LIMIT as u32), None) + .expect("LIMIT 100 sits exactly on the ceiling and must be accepted"); + assert_eq!(at_limit.k, MAX_RANKED_LIMIT); +} + +/// `LIMIT` is mandatory in ranked mode. There is no server-side default +/// because `k` is echoed inside the proof envelope and re-checked by the +/// verifier: a number the client never chose is a number it cannot +/// reproduce when rebuilding the query to verify. +#[test] +fn limit_is_required() { + let error = detect_avg(false, None, None).expect_err( + "a ranked query without a limit is \ + incomplete", + ); + match error { + Error::Query(QuerySyntaxError::InvalidLimit(message)) => { + assert!( + message.contains("limit"), + "the rejection must name the missing knob, got: {message}" + ); + } + other => panic!("expected InvalidLimit, got {other}"), + } + + // And an OFFSET alone does not stand in for it. + assert!(matches!( + detect_avg(false, None, Some(4)), + Err(Error::Query(QuerySyntaxError::InvalidLimit(_))) + )); +} + +/// Ranked indexes are single-property, so grouping is single-property +/// too — neither a missing nor a compound `GROUP BY` has a ranking to +/// resolve to. +#[test] +fn group_by_must_name_exactly_one_property() { + for group_by in [vec![], vec!["a".to_string(), "b".to_string()]] { + let error = detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by, + &[], + &order_by("grade", false), + &[], + page(Some(2), None), + ) + .expect_err("group_by arity other than 1 must be rejected"); + assert!(matches!( + error, + Error::Query(QuerySyntaxError::InvalidParameter(_)) + )); + } +} + +/// The `ORDER BY` field must be the *selected aggregate's* field: +/// ranking one aggregate while projecting another, or ordering groups by +/// a raw document property, would need a sort the axis secondary does +/// not maintain. +#[test] +fn order_by_must_name_the_selected_aggregate() { + // A different (perfectly real) document property. + let wrong_field = detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by(GROUP_PROPERTY, false), + &[], + page(Some(2), None), + ) + .expect_err("ordering groups by the grouping property is not a ranking"); + match wrong_field { + Error::Query(QuerySyntaxError::Unsupported(message)) => { + assert!( + message.contains("grade"), + "the rejection must name the ordering that *is* supported, got: {message}" + ); + } + other => panic!("expected an Unsupported query error, got {other}"), + } + + // Another numeric property the select does not aggregate. + assert!(matches!( + detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by("price", false), + &[], + page(Some(2), None), + ), + Err(Error::Query(QuerySyntaxError::Unsupported(_))) + )); + + // The COUNT(*) sentinel is not a stand-in for an AVG ordering. + assert!(matches!( + detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by(RANKED_COUNT_ORDER_KEY, false), + &[], + page(Some(2), None), + ), + Err(Error::Query(QuerySyntaxError::Unsupported(_))) + )); +} + +/// Exactly one `ORDER BY` clause. The axis secondary is a single-key +/// ordering — a second sort key would need a second axis the storage +/// does not maintain — and zero clauses is not a ranking at all. +#[test] +fn exactly_one_order_by_clause_is_required() { + let mut two = order_by("grade", false); + two.extend(order_by(GROUP_PROPERTY, true)); + for clauses in [Vec::new(), two] { + let error = detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &clauses, + &[], + page(Some(2), None), + ) + .expect_err("order_by must carry exactly one clause"); + assert!(matches!( + error, + Error::Query(QuerySyntaxError::InvalidParameter(_)) + )); + } +} + +/// A boolean `HAVING` cannot yet be combined with an aggregate +/// ordering: ranking reads a pre-sorted secondary, and filtering groups +/// first would mean walking every group to test the predicate — exactly +/// the cost the ranked surface exists to avoid. +#[test] +fn having_is_rejected_alongside_an_aggregate_ordering() { + let clauses = vec![HavingClause { + aggregate: HavingAggregate { + function: HavingAggregateFunction::Avg, + field: "grade".to_string(), + }, + operator: HavingOperator::GreaterThan, + right: HavingRightOperand::Value(Value::U64(80)), + }]; + let error = detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &clauses, + &order_by("grade", false), + &[], + page(Some(2), None), + ) + .expect_err("boolean having is not implemented on the ranked path"); + match error { + Error::Query(QuerySyntaxError::Unsupported(message)) => { + assert!( + message.contains("having"), + "the rejection must name the clause it is refusing, got: {message}" + ); + } + other => panic!("expected an Unsupported query error, got {other}"), + } +} + +/// Only the three maintained axes can be ranked, and `COUNT` must be +/// `COUNT(*)`: a ranked count axis counts documents per group. +#[test] +fn only_count_star_sum_and_avg_selects_are_rankable() { + for select in [ + SelectProjection::documents(), + SelectProjection::min("grade"), + SelectProjection::max("grade"), + SelectProjection::count_field("grade"), + ] { + let function = select.function; + let error = detect_ranked_mode_v0( + &select, + &group_by(), + &[], + &order_by("grade", false), + &[], + page(Some(2), None), + ) + .expect_err("only COUNT(*) / SUM(f) / AVG(f) rank"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "{function:?} must be rejected as unsupported, got {error}" + ); + } +} + +/// `SUM` / `AVG` need a field to aggregate; the index's `summable` +/// declaration is what they are matched against. +#[test] +fn sum_and_avg_selects_require_a_field() { + for function in [SelectFunction::Sum, SelectFunction::Avg] { + let select = SelectProjection { + function, + field: String::new(), + }; + let error = detect_ranked_mode_v0( + &select, + &group_by(), + &[], + &order_by("amount", false), + &[], + page(Some(2), None), + ) + .expect_err("a fieldless SUM/AVG has nothing to aggregate"); + assert!(matches!( + error, + Error::Query(QuerySyntaxError::InvalidParameter(_)) + )); + } +} + +/// A `where` clause is refused, not ignored. The axis secondary is +/// ordered by aggregate, not by group key, so it cannot rank a filtered +/// subset — silently dropping the filter would answer the unfiltered +/// question under the guise of the filtered one. +#[test] +fn where_clauses_are_rejected() { + let where_clauses = vec![WhereClause { + field: GROUP_PROPERTY.to_string(), + operator: WhereOperator::Equal, + value: Value::Text("alpha".to_string()), + }]; + let error = detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by("grade", false), + &where_clauses, + page(Some(2), None), + ) + .expect_err("ranked queries take no where clauses"); + assert!(matches!( + error, + Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(_)) + )); +} + +/// `start_at` / `start_after` name a document id, and document ids do +/// not appear in a keyspace sorted by aggregate — so the cursor is +/// refused while `OFFSET`, which *is* meaningful there, is accepted. +#[test] +fn start_at_is_rejected_while_offset_is_accepted() { + let error = detect_ranked_mode_v0( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by("grade", false), + &[], + RankedPaginationInputs { + limit: Some(2), + offset: None, + has_start_at: true, + }, + ) + .expect_err("ranked queries take no cursor"); + match error { + Error::Query(QuerySyntaxError::InvalidLimit(message)) => { + assert!( + message.contains("OFFSET"), + "the rejection must point at the pagination that does work, got: {message}" + ); + } + other => panic!("expected InvalidLimit, got {other}"), + } + + // The same request paginated by offset instead is accepted. + assert_eq!( + detect_avg(false, Some(2), Some(6)) + .expect("OFFSET is the ranked pagination") + .offset, + 6 + ); +} + +/// The versioned wrapper routes v0 to the v0 table and fails closed on an +/// unknown slot value rather than silently falling back. +#[test] +fn versioned_detection_routes_v0_and_rejects_unknown_versions() { + let versioned = detect_ranked_mode( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by("grade", false), + &[], + page(Some(4), None), + platform_version(), + ) + .expect("PV14's detect_ranked_mode slot is 0"); + assert_eq!(versioned, detect_avg(false, Some(4), None).unwrap()); + + let mut future = platform_version().clone(); + future.drive.methods.document.query.detect_ranked_mode = 1; + let error = detect_ranked_mode( + &SelectProjection::avg("grade"), + &group_by(), + &[], + &order_by("grade", false), + &[], + page(Some(4), None), + &future, + ) + .expect_err("an unknown routing-table version must fail closed"); + assert!(matches!( + error, + Error::Query(QuerySyntaxError::Unsupported(_)) + )); +} + +// =================================================================== +// Grammar: index picking +// =================================================================== + +fn test_index(name: &str, properties: &[&str], summable: Option<&str>) -> Index { + Index { + name: name.to_string(), + properties: properties + .iter() + .map(|property| IndexProperty { + name: property.to_string(), + ascending: true, + }) + .collect(), + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::Countable, + range_countable: true, + summable: summable.map(String::from), + range_summable: summable.is_some(), + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, + } +} + +fn index_map(indexes: Vec) -> BTreeMap { + indexes + .into_iter() + .map(|index| (index.name.clone(), index)) + .collect() +} + +/// The axis must be declared by the index, and it is the `ranked_*` flags +/// that decide — not the stored element variant. A `rankedCountable` +/// index that also declares `rangeSummable` is laid down as a PCPSIT +/// (which *can* host a sum secondary) yet carries only the Count axis, so +/// reading rankability off the element would over-report it. +#[test] +fn picker_requires_the_index_to_declare_the_requested_axis() { + let mut index = test_index("byRestaurant", &[GROUP_PROPERTY], Some("grade")); + index.ranked_countable = true; + let indexes = index_map(vec![index]); + + assert!( + find_ranked_index_for_axis(&indexes, GROUP_PROPERTY, RankedAxis::Count, "").is_some(), + "the declared axis resolves" + ); + for (axis, field) in [(RankedAxis::Sum, "grade"), (RankedAxis::Avg, "grade")] { + assert!( + find_ranked_index_for_axis(&indexes, GROUP_PROPERTY, axis, field).is_none(), + "{axis:?} is not declared even though the index is summable and the stored \ + element could host that secondary" + ); + } +} + +/// `SUM` / `AVG` rank the sum the index accumulates. Asking for a +/// different field must not resolve — it would answer about the wrong +/// property with no indication anything was substituted. +#[test] +fn picker_requires_the_select_field_to_be_the_indexed_summable() { + let mut index = test_index("byRestaurant", &[GROUP_PROPERTY], Some("grade")); + index.ranked_summable = true; + index.ranked_averageable = true; + let indexes = index_map(vec![index]); + + for axis in [RankedAxis::Sum, RankedAxis::Avg] { + assert!( + find_ranked_index_for_axis(&indexes, GROUP_PROPERTY, axis, "grade").is_some(), + "{axis:?} on the indexed summable resolves" + ); + assert!( + find_ranked_index_for_axis(&indexes, GROUP_PROPERTY, axis, "tipAmount").is_none(), + "{axis:?} on a different field must not resolve" + ); + } +} + +/// A property no ranked index groups on — whether it exists on the +/// doctype or not — has no ranking to serve. +#[test] +fn picker_rejects_an_unknown_group_property() { + let mut index = test_index("byRestaurant", &[GROUP_PROPERTY], Some("grade")); + index.ranked_averageable = true; + let indexes = index_map(vec![index]); + + assert!( + find_ranked_index_for_axis(&indexes, "chefId", RankedAxis::Avg, "grade").is_none(), + "no index groups by `chefId`" + ); +} + +/// Compound indexes cannot serve a ranked query even if they carry the +/// flags: their terminal property-name tree sits under a prefix value +/// tree, and naming that prefix would need a `where` clause the surface +/// does not accept. rs-dpp rejects such contracts at parse time; the +/// picker is the query-side backstop. +#[test] +fn picker_rejects_compound_indexes() { + let mut index = test_index( + "byRestaurantCourse", + &[GROUP_PROPERTY, "course"], + Some("price"), + ); + index.ranked_averageable = true; + let indexes = index_map(vec![index]); + + assert!( + find_ranked_index_for_axis(&indexes, GROUP_PROPERTY, RankedAxis::Avg, "price").is_none() + ); +} + +// =================================================================== +// Behaviour: end to end against the restaurants fixture +// =================================================================== + +/// Load and apply the restaurants fixture. +fn setup_restaurants() -> (Drive, DataContract) { + let drive = setup_drive_with_initial_state_structure(None); + let pv = platform_version(); + let contract = json_document_to_contract( + "tests/supporting_files/contract/restaurants/restaurants-contract.json", + false, + pv, + ) + .expect("expected to parse the restaurants contract"); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply the restaurants contract"); + (drive, contract) +} + +/// Insert `(restaurant, value)` rows as documents of `document_type_name` +/// through the ordinary document-insert path, so the ranked secondaries +/// are maintained by the real write path. +/// +/// `first_seed` seeds the random-document generator, which is what +/// derives each document's id. Two calls in the same test must pass +/// disjoint seed ranges or the second one collides on an existing id. +fn insert_docs( + drive: &Drive, + contract: &DataContract, + document_type_name: &str, + aggregated_property: &str, + first_seed: u64, + rows: &[(&str, i64)], +) { + let pv = platform_version(); + let document_type = contract + .document_type_for_name(document_type_name) + .unwrap_or_else(|_| panic!("{document_type_name} doctype exists")); + for (i, (restaurant, value)) in rows.iter().enumerate() { + let mut doc: Document = document_type + .random_document(Some(first_seed + i as u64), pv) + .expect("random document"); + let mut props = BTreeMap::new(); + props.insert( + GROUP_PROPERTY.to_string(), + Value::Text(restaurant.to_string()), + ); + props.insert(aggregated_property.to_string(), Value::I64(*value)); + doc.set_properties(props); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&doc, None)), + owner_id: None, + }, + contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .unwrap_or_else(|e| panic!("expected to insert a {document_type_name} document: {e}")); + } +} + +/// One ranked request, minus the `prove` flag and the pieces the +/// dispatcher derives. +#[derive(Clone)] +struct RankedCase { + document_type_name: &'static str, + /// The single `GROUP BY` property. Every fixture doctype groups by + /// [`GROUP_PROPERTY`]; the multi-index contract below is the one that + /// varies it, which is what makes the index picker's choice + /// observable. + group_by_property: &'static str, + select: SelectProjection, + /// The `ORDER BY` field. Must name the selected aggregate: + /// [`RANKED_COUNT_ORDER_KEY`] for `COUNT(*)`, otherwise the select's + /// own field. + order_field: &'static str, + /// `false` is `DESC` — the "highest first" ranking. + ascending: bool, + limit: Option, + offset: Option, +} + +impl RankedCase { + fn avg(ascending: bool, limit: Option) -> Self { + Self { + document_type_name: "review", + group_by_property: GROUP_PROPERTY, + select: SelectProjection::avg("grade"), + order_field: "grade", + ascending, + limit, + offset: None, + } + } + + fn count(ascending: bool, limit: Option) -> Self { + Self { + document_type_name: "visit", + group_by_property: GROUP_PROPERTY, + select: SelectProjection::count_star(), + order_field: RANKED_COUNT_ORDER_KEY, + ascending, + limit, + offset: None, + } + } + + fn sum(ascending: bool, limit: Option) -> Self { + Self { + document_type_name: "tip", + group_by_property: GROUP_PROPERTY, + select: SelectProjection::sum("amount"), + order_field: "amount", + ascending, + limit, + offset: None, + } + } + + /// The same case, paginated: `… LIMIT limit OFFSET offset`. + fn at_offset(mut self, offset: u32) -> Self { + self.offset = Some(offset); + self + } + + fn group_by(&self) -> Vec { + vec![self.group_by_property.to_string()] + } + + fn order_by(&self) -> Vec { + vec![OrderClause { + field: self.order_field.to_string(), + ascending: self.ascending, + }] + } +} + +/// Run a case through the public dispatcher entry point — the same call +/// drive-abci's routing layer will make. +fn run( + drive: &Drive, + contract: &DataContract, + case: &RankedCase, + prove: bool, +) -> Result { + let group_by = case.group_by(); + let order_by = case.order_by(); + let document_type = contract + .document_type_for_name(case.document_type_name) + .expect("doctype exists"); + drive.execute_document_ranked_request( + DocumentRankedRequest { + contract, + document_type, + group_by: &group_by, + select: case.select.clone(), + having: &[], + order_by: &order_by, + where_clauses: &[], + limit: case.limit, + offset: case.offset, + has_start_at: false, + prove, + }, + None, + platform_version(), + ) +} + +fn page_of(response: DocumentRankedResponse) -> RankedPage { + match response { + DocumentRankedResponse::Entries(page) => page, + DocumentRankedResponse::Proof(_) => panic!("expected entries, got a proof"), + } +} + +fn entries_of(response: DocumentRankedResponse) -> Vec { + page_of(response).entries +} + +fn proof_of(response: DocumentRankedResponse) -> Vec { + match response { + DocumentRankedResponse::Proof(proof) => proof, + DocumentRankedResponse::Entries(_) => panic!("expected a proof, got entries"), + } +} + +fn keys_of(entries: &[RankedEntry]) -> Vec { + entries + .iter() + .map(|entry| String::from_utf8(entry.key.clone()).expect("fixture group keys are utf-8")) + .collect() +} + +/// Rebuild the query the way a client would: re-run the same versioned +/// validation, then resolve the index off the contract. This is the shape +/// the SDK's proof helper will take, so exercising it here pins that the +/// verifier can reach everything it needs from public API. +fn client_side_query<'a>( + contract: &'a DataContract, + case: &RankedCase, +) -> DriveDocumentRankedQuery<'a> { + let group_by = case.group_by(); + let order_by = case.order_by(); + let mode = detect_ranked_mode( + &case.select, + &group_by, + &[], + &order_by, + &[], + RankedPaginationInputs { + limit: case.limit, + offset: case.offset, + has_start_at: false, + }, + platform_version(), + ) + .expect("the case is well-formed"); + // Taken off the contract's document-type map rather than off a local + // `DocumentTypeRef`, so the borrow lives as long as `contract`. + let indexes = contract + .document_types() + .get(case.document_type_name) + .expect("doctype exists") + .indexes(); + let index = find_ranked_index_for_axis( + indexes, + &mode.group_by_property, + mode.axis, + &mode.aggregate_field, + ) + .expect("the fixture declares the axis"); + DriveDocumentRankedQuery { + document_type: contract + .document_type_for_name(case.document_type_name) + .expect("doctype exists"), + contract_id: contract.id_ref().to_buffer(), + document_type_name: case.document_type_name.to_string(), + index, + axis: mode.axis, + descending: mode.descending, + k: mode.k, + offset: mode.offset, + } +} + +fn grovedb_root_hash(drive: &Drive) -> [u8; 32] { + drive + .grove + .root_hash(None, &platform_version().drive.grove_version) + .unwrap() + .expect("root hash must be readable") +} + +/// Prove the case, verify the proof, and assert the verified page and +/// root hash match the live database. Returns the verified page so +/// callers can assert on the attested `skipped` rank. +fn assert_proof_round_trips( + drive: &Drive, + contract: &DataContract, + case: &RankedCase, + expected: &[RankedEntry], +) -> RankedPage { + let proof = proof_of(run(drive, contract, case, true).expect("prove must succeed")); + let query = client_side_query(contract, case); + let (root_hash, verified) = query + .verify_ranked_top_k_proof(&proof, platform_version()) + .expect("the proof must verify"); + assert_eq!( + verified.entries, expected, + "verified entries must equal what the unproven read returned" + ); + assert_eq!( + root_hash, + grovedb_root_hash(drive), + "the proof must reconstruct the live grovedb root hash" + ); + verified +} + +/// Averages: alpha (90+80)/2 = 85, beta (60+70+50)/3 = 60, gamma 95, +/// delta (40+20)/2 = 30. The fixed-point values are asserted explicitly +/// rather than compared to a re-derived helper's output, so a change to +/// the scale or the rounding shows up here. +#[test] +fn avg_axis_ranks_reads_and_proves_consistently() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "review", + "grade", + 1, + &[ + ("alpha", 90), + ("alpha", 80), + ("beta", 60), + ("beta", 70), + ("beta", 50), + ("gamma", 95), + ("delta", 40), + ("delta", 20), + ], + ); + + let top_three = RankedCase::avg(false, Some(3)); + let entries = entries_of(run(&drive, &contract, &top_three, false).expect("read must succeed")); + assert_eq!( + keys_of(&entries), + vec!["gamma", "alpha", "beta"], + "descending by average: gamma(95) > alpha(85) > beta(60) > delta(30)" + ); + assert_eq!( + entries[0].value, + RankedEntryValue::AvgFixedPoint(95 * RANKED_AVG_SCALE) + ); + assert_eq!( + entries[1].value, + RankedEntryValue::AvgFixedPoint(85 * RANKED_AVG_SCALE) + ); + assert_eq!( + entries[2].value, + RankedEntryValue::AvgFixedPoint(60 * RANKED_AVG_SCALE) + ); + assert_proof_round_trips(&drive, &contract, &top_three, &entries); + + // A non-integral average exercises the fixed-point floor: beta's + // 180/3 is exact, so use a group whose sum doesn't divide evenly. + insert_docs( + &drive, + &contract, + "review", + "grade", + 500, + &[("epsilon", 10), ("epsilon", 11)], + ); + let bottom_one = RankedCase::avg(true, Some(1)); + let entries = + entries_of(run(&drive, &contract, &bottom_one, false).expect("read must succeed")); + assert_eq!( + keys_of(&entries), + vec!["epsilon"], + "21/2 = 10.5 is the lowest average" + ); + assert_eq!( + entries[0].value, + RankedEntryValue::AvgFixedPoint(21 * RANKED_AVG_SCALE / 2), + "the Avg axis sorts by floor(sum * RANKED_AVG_SCALE / count)" + ); + assert_eq!( + entries[0].value, + RankedEntryValue::AvgFixedPoint(compute_avg_fixed_point(21, 2)), + "and that is exactly grovedb's own fixed-point computation" + ); + assert_eq!( + entries[0].value.as_f64(), + 10.5, + "as_f64 divides the fixed point back down by the scale" + ); + assert_proof_round_trips(&drive, &contract, &bottom_one, &entries); +} + +/// The Count axis ranks by group size; `COUNT(*)` needs no field. +#[test] +fn count_axis_ranks_reads_and_proves_consistently() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "visit", + "guests", + 1, + &[ + ("alpha", 2), + ("beta", 4), + ("beta", 2), + ("beta", 6), + ("gamma", 3), + ("gamma", 1), + ("delta", 5), + ("delta", 5), + ("delta", 5), + ("delta", 5), + ], + ); + + let top_two = RankedCase::count(false, Some(2)); + let entries = entries_of(run(&drive, &contract, &top_two, false).expect("read must succeed")); + assert_eq!( + keys_of(&entries), + vec!["delta", "beta"], + "descending by document count: delta(4) > beta(3) > gamma(2) > alpha(1)" + ); + assert_eq!(entries[0].value, RankedEntryValue::Count(4)); + assert_eq!(entries[1].value, RankedEntryValue::Count(3)); + assert_proof_round_trips(&drive, &contract, &top_two, &entries); + + let bottom_one = RankedCase::count(true, Some(1)); + let entries = + entries_of(run(&drive, &contract, &bottom_one, false).expect("read must succeed")); + assert_eq!( + keys_of(&entries), + vec!["alpha"], + "ASC LIMIT 1 is the single smallest group" + ); + assert_eq!(entries[0].value, RankedEntryValue::Count(1)); + assert_proof_round_trips(&drive, &contract, &bottom_one, &entries); + + // A missing LIMIT is refused end to end, not just in the pure + // detector: `k` is echoed in the proof envelope, so there is no + // server-side default a verifying client could reproduce. + let mut no_limit = RankedCase::count(true, None); + no_limit.limit = None; + let error = run(&drive, &contract, &no_limit, false) + .expect_err("a ranked request without a limit is incomplete"); + assert!(matches!( + error, + Error::Query(QuerySyntaxError::InvalidLimit(_)) + )); +} + +/// The Sum axis ranks by the running sum of the index's summable property. +#[test] +fn sum_axis_ranks_reads_and_proves_consistently() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "tip", + "amount", + 1, + &[ + ("alpha", 10), + ("alpha", 15), + ("beta", 100), + ("gamma", 7), + ("gamma", 8), + ("gamma", 9), + ("delta", 1), + ], + ); + + let top_one = RankedCase::sum(false, Some(1)); + let entries = entries_of(run(&drive, &contract, &top_one, false).expect("read must succeed")); + assert_eq!( + keys_of(&entries), + vec!["beta"], + "DESC LIMIT 1 is the single largest sum" + ); + assert_eq!(entries[0].value, RankedEntryValue::Sum(100)); + assert_proof_round_trips(&drive, &contract, &top_one, &entries); + + let bottom_three = RankedCase::sum(true, Some(3)); + let entries = + entries_of(run(&drive, &contract, &bottom_three, false).expect("read must succeed")); + assert_eq!( + keys_of(&entries), + vec!["delta", "gamma", "alpha"], + "ascending by sum: delta(1) < gamma(24) < alpha(25) < beta(100)" + ); + assert_eq!( + entries.iter().map(|entry| entry.value).collect::>(), + vec![ + RankedEntryValue::Sum(1), + RankedEntryValue::Sum(24), + RankedEntryValue::Sum(25) + ] + ); + assert_proof_round_trips(&drive, &contract, &bottom_three, &entries); +} + +/// Asking for more groups than exist returns them all — a short result is +/// the index having fewer groups, not an error, and the proof round-trips +/// just the same. +#[test] +fn top_k_larger_than_the_group_count_returns_every_group() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "tip", + "amount", + 1, + &[("alpha", 10), ("beta", 20)], + ); + + let case = RankedCase::sum(false, Some(MAX_RANKED_LIMIT as u32)); + let entries = entries_of(run(&drive, &contract, &case, false).expect("read must succeed")); + assert_eq!(keys_of(&entries), vec!["beta", "alpha"]); + assert_proof_round_trips(&drive, &contract, &case, &entries); +} + +/// **`OFFSET` pages through the ranking, and the proof attests where the +/// page starts.** +/// +/// Five groups by average grade, descending: +/// `gamma(95) > alpha(85) > beta(60) > delta(30) > epsilon(10)`. +/// +/// Three shapes are covered, because they fail differently: +/// +/// 1. **A one-entry window in the middle** — `LIMIT 1 OFFSET 4` is how +/// "the 5th best grade" is spelled. The entry alone does not say +/// which rank it is; `skipped` does, and it comes back attested. +/// 2. **A window running off the end** — asking for three groups from +/// rank 3 yields the two that exist. A short page is the index +/// having fewer groups, not an error, exactly as a short unpaginated +/// page is. +/// 3. **A window entirely past the end** — the page is empty *and* +/// `skipped` collapses to the secondary's true population, which is +/// the proof's way of saying "there is nothing here, and here is how +/// much there is in total". That is the one case where the proved +/// and unproven paths differ: the unproven read cannot see the short +/// walk and reports the requested offset. +#[test] +fn offset_pages_through_the_ranking_and_the_proof_attests_the_starting_rank() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "review", + "grade", + 1, + &[ + ("gamma", 95), + ("alpha", 90), + ("alpha", 80), + ("beta", 60), + ("delta", 30), + ("epsilon", 10), + ], + ); + + // (1) The 5th best grade: skip the four above it, take one. + let fifth_best = RankedCase::avg(false, Some(1)).at_offset(4); + let page = page_of(run(&drive, &contract, &fifth_best, false).expect("read must succeed")); + assert_eq!( + keys_of(&page.entries), + vec!["epsilon"], + "gamma > alpha > beta > delta > epsilon — rank 4 (0-based) is epsilon" + ); + assert_eq!(page.entries[0].value.as_f64(), 10.0); + let verified = assert_proof_round_trips(&drive, &contract, &fifth_best, &page.entries); + assert_eq!( + verified.skipped, 4, + "the proof must attest that this entry really is the 5th-ranked group, not just \ + that it is *a* group" + ); + + // (2) A window that runs off the end returns the tail, short. + let tail = RankedCase::avg(false, Some(3)).at_offset(3); + let page = page_of(run(&drive, &contract, &tail, false).expect("read must succeed")); + assert_eq!( + keys_of(&page.entries), + vec!["delta", "epsilon"], + "only two groups remain from rank 3; a short page is not an error" + ); + let verified = assert_proof_round_trips(&drive, &contract, &tail, &page.entries); + assert_eq!( + verified.skipped, 3, + "the skip itself succeeded, so `skipped` is the requested offset" + ); + + // (3) A window entirely past the end: empty page, and `skipped` + // attests the population. + let past_end = RankedCase::avg(false, Some(2)).at_offset(9); + let page = page_of(run(&drive, &contract, &past_end, false).expect("read must succeed")); + assert!( + page.entries.is_empty(), + "there is no rank 9 in a five-group ranking" + ); + assert_eq!( + page.skipped, 9, + "the unproven read cannot see the short walk, so it echoes the requested offset" + ); + let verified = assert_proof_round_trips(&drive, &contract, &past_end, &page.entries); + assert_eq!( + verified.skipped, 5, + "the *proved* path re-derives the real skip from the counted subtree \ + commitments, and `skipped < offset` with an empty page is a proof that the \ + ranking holds exactly five groups" + ); + + // Paging with `ASC` walks the same five groups from the other end, + // so offset 4 there is the *best* group rather than the worst. + let worst_first_fifth = RankedCase::avg(true, Some(1)).at_offset(4); + let page = + page_of(run(&drive, &contract, &worst_first_fifth, false).expect("read must succeed")); + assert_eq!(keys_of(&page.entries), vec!["gamma"]); + let verified = assert_proof_round_trips(&drive, &contract, &worst_first_fifth, &page.entries); + assert_eq!(verified.skipped, 4); +} + +/// A proof of one page must not verify as another page of the same +/// ranking. `offset` is echoed in the envelope and re-checked, which is +/// what stops a server from answering "the 5th best" with a proof of +/// "the best" — the entries would look perfectly valid, and only the +/// offset binding distinguishes them. +#[test] +fn a_proof_does_not_verify_under_a_different_offset() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "review", + "grade", + 1, + &[("alpha", 90), ("beta", 60), ("gamma", 30), ("delta", 10)], + ); + + let at_two = RankedCase::avg(false, Some(1)).at_offset(2); + let proof = proof_of(run(&drive, &contract, &at_two, true).expect("prove must succeed")); + assert!(client_side_query(&contract, &at_two) + .verify_ranked_top_k_proof(&proof, platform_version()) + .is_ok()); + + for other_offset in [0u32, 1, 3] { + let other = RankedCase::avg(false, Some(1)).at_offset(other_offset); + assert!( + client_side_query(&contract, &other) + .verify_ranked_top_k_proof(&proof, platform_version()) + .is_err(), + "a proof for OFFSET 2 must not verify as OFFSET {other_offset}" + ); + } + + // And an unpaginated query is not the same query as `OFFSET 0`'s + // sibling either — it *is* `OFFSET 0`, so that one must verify. + let unpaginated_proof = proof_of( + run(&drive, &contract, &RankedCase::avg(false, Some(1)), true).expect("prove must succeed"), + ); + assert!( + client_side_query(&contract, &RankedCase::avg(false, Some(1)).at_offset(0)) + .verify_ranked_top_k_proof(&unpaginated_proof, platform_version()) + .is_ok(), + "an absent OFFSET and `OFFSET 0` are the same request, so one's proof verifies \ + under the other" + ); +} + +/// **An empty ranking now proves, and that is a consequence of moving to +/// the paginated primitive.** +/// +/// The unproven read returns an empty list — the indexed tree exists from +/// contract registration, its secondary is simply empty. Under the old +/// non-paginated prover the prove path *errored* here ("Cannot create +/// proof for empty tree"): there was no envelope shape for "this ranking +/// has no entries", so a freshly registered contract queried with +/// `prove = true` got an error until the first document landed. +/// +/// `prove_indexed_axis_top_k_paginated` closes that gap — it emits a +/// guaranteed-empty range against the secondary rather than refusing — +/// so the two paths now agree on empty state, and this test is the +/// tripwire that says so. The attested `skipped` is `0`, which for an +/// `OFFSET 0` request is the proof that the ranking is empty rather than +/// merely unread. +#[test] +fn ranking_an_empty_index_reads_empty_and_proves_empty() { + let (drive, contract) = setup_restaurants(); + let case = RankedCase::avg(false, Some(5)); + + let entries = entries_of(run(&drive, &contract, &case, false).expect("read must succeed")); + assert!( + entries.is_empty(), + "an index with no documents has no groups to rank" + ); + + let verified = assert_proof_round_trips(&drive, &contract, &case, &[]); + assert_eq!( + verified.skipped, 0, + "nothing was skipped because there was nothing to skip" + ); + + // Asking past the end of an empty ranking is equally provable, and + // `skipped` collapses to the (zero) population. + let verified = assert_proof_round_trips( + &drive, + &contract, + &RankedCase::avg(false, Some(5)).at_offset(3), + &[], + ); + assert_eq!( + verified.skipped, 0, + "the walk found nothing to skip, which attests a population of 0" + ); + + // And the same request keeps working once a document lands. + insert_docs(&drive, &contract, "review", "grade", 1, &[("alpha", 42)]); + let entries = entries_of(run(&drive, &contract, &case, false).expect("read must succeed")); + assert_eq!(keys_of(&entries), vec!["alpha"]); + assert_proof_round_trips(&drive, &contract, &case, &entries); +} + +/// **Tie ordering, pinned against observed grovedb behaviour.** +/// +/// The secondary's keys are `(sort_key ‖ group_key)` and the walk is a +/// plain directional scan of that keyspace, so equal aggregates come back +/// in group-key order *in the direction of the walk*: ascending group key +/// going up, descending group key going down. The descending case is the +/// counter-intuitive one — a caller expecting "ties always break +/// ascending by key" would read the list backwards — so both directions +/// are asserted here rather than only the natural one. +#[test] +fn ties_break_by_group_key_in_the_walk_direction() { + let (drive, contract) = setup_restaurants(); + // Four groups, all summing to 50: only the group key distinguishes them. + insert_docs( + &drive, + &contract, + "tip", + "amount", + 1, + &[("alpha", 50), ("beta", 50), ("gamma", 50), ("delta", 50)], + ); + + let descending = entries_of( + run(&drive, &contract, &RankedCase::sum(false, Some(4)), false).expect("read must succeed"), + ); + assert_eq!( + keys_of(&descending), + vec!["gamma", "delta", "beta", "alpha"], + "descending walk breaks ties in DESCENDING group-key order" + ); + + let ascending = entries_of( + run(&drive, &contract, &RankedCase::sum(true, Some(4)), false).expect("read must succeed"), + ); + assert_eq!( + keys_of(&ascending), + vec!["alpha", "beta", "delta", "gamma"], + "ascending walk breaks ties in ascending group-key order" + ); + assert_eq!( + ascending.iter().rev().map(|e| &e.key).collect::>(), + descending.iter().map(|e| &e.key).collect::>(), + "the two directions are exact reverses of each other under a full-width k" + ); + + // A tie-truncating `k` therefore selects a *specific* subset, not an + // arbitrary one — which is what makes a truncated LIMIT k reproducible. + let top_two = entries_of( + run(&drive, &contract, &RankedCase::sum(false, Some(2)), false).expect("read must succeed"), + ); + assert_eq!(keys_of(&top_two), vec!["gamma", "delta"]); + assert_proof_round_trips( + &drive, + &contract, + &RankedCase::sum(false, Some(2)), + &top_two, + ); +} + +/// A tampered proof must never verify **to the honest root hash**. +/// +/// That phrasing is the actual security property, and it is stronger than +/// "must return `Err`". Sweeping every byte of a real envelope shows why: +/// most flips do error out, but ~9% of them (the bytes of sibling-subtree +/// hashes inside the ancestor layer proofs) verify cleanly and return the +/// correct entries — with a *different* reconstructed root hash. Those +/// tampers are caught downstream, when the caller compares the returned +/// root against the tenderdash-signed app hash. +/// +/// So: `verify_ranked_top_k_proof` returning `Ok` is not by itself +/// evidence of anything. The returned `RootHash` is load-bearing and +/// every consumer must bind it to consensus — see +/// `rs-drive-proof-verifier` for the composition that does. This test +/// exists to keep that from being forgotten, and to prove the envelope +/// leaves no byte that can be changed while *both* the entries and the +/// root hash survive. +#[test] +fn a_tampered_proof_never_verifies_to_the_honest_root_hash() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "review", + "grade", + 1, + &[("alpha", 90), ("beta", 60), ("gamma", 30)], + ); + + let case = RankedCase::avg(false, Some(2)); + let proof = proof_of(run(&drive, &contract, &case, true).expect("prove must succeed")); + let query = client_side_query(&contract, &case); + let (_, honest) = query + .verify_ranked_top_k_proof(&proof, platform_version()) + .expect("the untampered proof verifies"); + + let honest_root = grovedb_root_hash(&drive); + let mut verified_at_all = 0usize; + for index in 0..proof.len() { + for bit in 0..8 { + let mut tampered = proof.clone(); + tampered[index] ^= 1 << bit; + if let Ok((root, entries)) = + query.verify_ranked_top_k_proof(&tampered, platform_version()) + { + verified_at_all += 1; + assert_ne!( + root, honest_root, + "flipping bit {bit} of byte {index} produced a proof that verified to \ + the honest root hash with entries {entries:?} (honest result was \ + {honest:?}) — the envelope must not have a byte that can be changed \ + while the reconstructed root survives" + ); + } + } + } + // Sanity-check the test itself: if *every* mutation errored out, the + // assertion above would be vacuous and a regression that made + // verification accept-everything-with-a-wrong-root would slip past. + assert!( + verified_at_all > 0, + "expected some mutations (sibling-hash bytes) to verify with a diverged root hash" + ); +} + +/// The verifier must be checking the ranking it was asked about: a proof +/// generated for one `(axis, k, descending)` triple must not verify under +/// another. grovedb echoes all three in the envelope and re-checks them, +/// and this pins that drive passes each of them through faithfully — a +/// dropped argument here would let a client accept a proof of a +/// different question. +#[test] +fn a_proof_does_not_verify_under_a_different_ranking() { + let (drive, contract) = setup_restaurants(); + insert_docs( + &drive, + &contract, + "review", + "grade", + 1, + &[("alpha", 90), ("beta", 60), ("gamma", 30)], + ); + + let case = RankedCase::avg(false, Some(2)); + let proof = proof_of(run(&drive, &contract, &case, true).expect("prove must succeed")); + let honest = client_side_query(&contract, &case); + assert!(honest + .verify_ranked_top_k_proof(&proof, platform_version()) + .is_ok()); + + let mut wrong_k = honest.clone(); + wrong_k.k = 3; + assert!( + wrong_k + .verify_ranked_top_k_proof(&proof, platform_version()) + .is_err(), + "a proof for k = 2 must not verify as k = 3" + ); + + let mut wrong_direction = honest.clone(); + wrong_direction.descending = false; + assert!( + wrong_direction + .verify_ranked_top_k_proof(&proof, platform_version()) + .is_err(), + "a TOP proof must not verify as a BOTTOM one" + ); + + // The `review` index declares only the Avg axis, so Count / Sum are + // not even provable against it — but a client that mislabels the axis + // must still be rejected rather than mis-decoding the entries. + for axis in [RankedAxis::Count, RankedAxis::Sum] { + let mut wrong_axis = honest.clone(); + wrong_axis.axis = axis; + assert!( + wrong_axis + .verify_ranked_top_k_proof(&proof, platform_version()) + .is_err(), + "an Avg proof must not verify as {axis:?}" + ); + } +} + +/// An unknown verify-method version fails closed rather than silently +/// falling back to v0. +#[test] +fn verify_rejects_an_unknown_method_version() { + let (drive, contract) = setup_restaurants(); + insert_docs(&drive, &contract, "review", "grade", 1, &[("alpha", 90)]); + + let case = RankedCase::avg(false, Some(1)); + let proof = proof_of(run(&drive, &contract, &case, true).expect("prove must succeed")); + let query = client_side_query(&contract, &case); + + let mut future = platform_version().clone(); + future + .drive + .methods + .verify + .document_ranked + .verify_ranked_top_k_proof = 1; + assert!(matches!( + query.verify_ranked_top_k_proof(&proof, &future), + Err(Error::Drive( + crate::error::drive::DriveError::UnknownVersionMismatch { .. } + )) + )); +} + +/// A request whose axis no index declares is rejected at dispatch with a +/// message naming the contract keyword that is missing — the `visit` +/// doctype ranks by count only, so asking it for a sum ranking cannot be +/// served. +#[test] +fn a_request_for_an_undeclared_axis_is_rejected_at_dispatch() { + let (drive, contract) = setup_restaurants(); + insert_docs(&drive, &contract, "visit", "guests", 1, &[("alpha", 2)]); + + let case = RankedCase { + document_type_name: "visit", + group_by_property: GROUP_PROPERTY, + select: SelectProjection::sum("guests"), + order_field: "guests", + ascending: false, + limit: Some(2), + offset: None, + }; + let error = run(&drive, &contract, &case, false) + .expect_err("the visit index declares only rankedCountable"); + match error { + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(message)) => { + assert!( + message.contains("rankedSummable"), + "the error must name the missing keyword, got: {message}" + ); + } + other => panic!("expected WhereClauseOnNonIndexedProperty, got {other}"), + } +} + +// =================================================================== +// Behaviour: picking between several indexes on one doctype +// =================================================================== + +/// The second grouping property of the multi-index contract. +const CHEF_PROPERTY: &str = "chefId"; + +/// A doctype carrying three indexes at once — the shape the shared +/// `restaurants` fixture cannot express, since it spends one doctype per +/// axis to dodge `DuplicateIndexError`: +/// +/// | index | properties | ranking | +/// |--------------------|--------------------------|------------------| +/// | `byChef` | `chefId` | Count | +/// | `byChefRestaurant` | `chefId`, `restaurantId` | none | +/// | `byRestaurant` | `restaurantId` | Avg over `grade` | +/// +/// Three things about this contract are load-bearing and none of them +/// are reachable from the fixture: +/// +/// 1. **Two ranked indexes, different axes, different properties.** The +/// picker has to choose on `(group property, axis, field)` rather +/// than "the doctype's one ranked index", and the two indexes land on +/// *different* grove paths and different stored tree types — +/// `byChef` is a `ProvableCountIndexedTree`, `byRestaurant` a +/// `ProvableCountProvableSumIndexedTree` carrying the Avg axis. +/// 2. **A non-ranked index over a ranked index's property.** Two indexes +/// on `[chefId]` alone would be a `DuplicateIndexError`, so the +/// non-ranked sibling is the compound `[chefId, restaurantId]` — +/// which is the sharper case anyway: index levels are merged into one +/// trie keyed by property name, so `byChefRestaurant` hangs a +/// `restaurantId` property-name tree underneath the very value trees +/// whose counts `byChef`'s ranked secondary is ranking. The write path +/// wraps those continuations `NonCounted`; if that wrapping were +/// wrong, the counts asserted below would drift. +/// 3. **The picker must not answer from the compound index.** It carries +/// `chefId` as its first property but no ranking, and it is +/// single-property-ness — not mere presence of the property — that +/// qualifies an index. +/// +/// The compound sibling hangs off `byChef` (count-only) rather than off +/// `byRestaurant` (count+sum) because that is the narrower shape for a +/// *picker* test: a count-only parent takes the `NonCounted` wrapper on +/// both sides of the v14 boundary, so this fixture reads identically +/// whichever index walker generation runs it. +/// +/// Hanging it off `byRestaurant` is no longer impossible, though. Until +/// v14 a plain `NormalTree` continuation under a count+sum value tree +/// was refused outright (the wrapper matrix had only its diagonal), which +/// made *any* summable index terminating at `[a]` incompatible with a +/// compound index `[a, …]` on the same doctype; the v14 shared-prefix fix +/// completes the matrix and demotes the value trees, so ranked and +/// compound indexes can now share a count+sum prefix. That combination is +/// covered end to end by +/// `ranked_index_ranks_correctly_next_to_a_compound_index_sharing_its_property` +/// in `drive::contract::insert::insert_contract::v0::tests::ranked_index_e2e_tests`; +/// keeping it out of this fixture keeps the picker assertions below about +/// index selection only. +fn multi_index_contract() -> DataContract { + use dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; + + let json = serde_json::json!({ + "$formatVersion": "0", + "id": "94zNLp7A1ZcYG3Egqf2YmQk4DQr9P8D543GwXyCJRz4", + "ownerId": "AcYUCSvAmUwryNsQqkqqD1o3BnFuzepGtR3Mhh2swLk6", + "version": 1, + "documentSchemas": { + "dish": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurant", + "properties": [{ "restaurantId": "asc" }], + "countable": "countable", + "summable": "grade", + "averageable": "grade", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true + }, + { + "name": "byChef", + "properties": [{ "chefId": "asc" }], + "countable": "countable", + "rangeCountable": true, + "rankedCountable": true + }, + { + "name": "byChefRestaurant", + "properties": [ + { "chefId": "asc" }, + { "restaurantId": "asc" } + ] + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "chefId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 1 + }, + "grade": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "position": 2 + } + }, + "required": ["restaurantId", "chefId", "grade"], + "additionalProperties": false + } + } + }); + + // Same non-validating load `setup_restaurants` gets through + // `json_document_to_contract`: the meta-schema acceptance of the + // ranked keywords is rs-dpp's to test, and re-running it here would + // only couple this test to the meta schema. + DataContract::from_json(json, false, platform_version()) + .expect("expected to parse the multi-index contract") +} + +/// Apply [`multi_index_contract`] to a fresh Drive. +fn setup_multi_index() -> (Drive, DataContract) { + let drive = setup_drive_with_initial_state_structure(None); + let contract = multi_index_contract(); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version(), + ) + .expect("expected to apply the multi-index contract"); + (drive, contract) +} + +/// Insert `(restaurant, chef, grade)` rows as `dish` documents through +/// the ordinary write path, so every index — ranked and not — is +/// maintained by the code under test. +fn insert_dishes(drive: &Drive, contract: &DataContract, rows: &[(&str, &str, i64)]) { + let pv = platform_version(); + let document_type = contract + .document_type_for_name("dish") + .expect("dish doctype exists"); + for (i, (restaurant, chef, grade)) in rows.iter().enumerate() { + let mut doc: Document = document_type + .random_document(Some(i as u64 + 1), pv) + .expect("random document"); + let mut props = BTreeMap::new(); + props.insert( + GROUP_PROPERTY.to_string(), + Value::Text(restaurant.to_string()), + ); + props.insert(CHEF_PROPERTY.to_string(), Value::Text(chef.to_string())); + props.insert("grade".to_string(), Value::I64(*grade)); + doc.set_properties(props); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&doc, None)), + owner_id: None, + }, + contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .unwrap_or_else(|e| panic!("expected to insert a dish document: {e}")); + } +} + +/// `SELECT AVG(grade) … GROUP BY restaurantId` against the multi-index +/// doctype. +fn dish_avg_case(ascending: bool, limit: Option) -> RankedCase { + RankedCase { + document_type_name: "dish", + group_by_property: GROUP_PROPERTY, + select: SelectProjection::avg("grade"), + order_field: "grade", + ascending, + limit, + offset: None, + } +} + +/// `SELECT COUNT(*) … GROUP BY chefId` against the multi-index doctype. +fn dish_count_case(ascending: bool, limit: Option) -> RankedCase { + RankedCase { + document_type_name: "dish", + group_by_property: CHEF_PROPERTY, + select: SelectProjection::count_star(), + order_field: RANKED_COUNT_ORDER_KEY, + ascending, + limit, + offset: None, + } +} + +fn dish_indexes(contract: &DataContract) -> &BTreeMap { + contract + .document_types() + .get("dish") + .expect("dish doctype exists") + .indexes() +} + +/// **The multi-index picking seam.** +/// +/// One doctype, three indexes, two of them ranked on different +/// properties and different axes. Each ranked request must resolve to +/// its own index — and the client, rebuilding the query from nothing but +/// the contract and the request, must resolve the *same* one, because +/// the index is what fixes the grove path the proof is read against. A +/// disagreement here is not a wrong answer, it is an unverifiable one. +/// +/// Data (restaurant, chef, grade): +/// +/// - alpha: 90, 80 → avg 85 +/// - beta: 60, 70, 50 → avg 60 +/// - gamma: 95 → avg 95 +/// +/// which by chef is ann = 3 dishes, bob = 2, cid = 1. Both rankings are +/// tie-free so the assertions pin the ranking, not the tie-break rule +/// (that is `ties_break_by_group_key_in_the_walk_direction`'s job). +#[test] +fn a_doctype_with_several_indexes_ranks_each_group_property_on_its_own_index() { + let (drive, contract) = setup_multi_index(); + insert_dishes( + &drive, + &contract, + &[ + ("alpha", "ann", 90), + ("alpha", "bob", 80), + ("beta", "ann", 60), + ("beta", "ann", 70), + ("beta", "bob", 50), + ("gamma", "cid", 95), + ], + ); + + // (a) Grouping by `restaurantId` resolves the Avg-ranked index and + // ranks by the average of `grade`. + let by_restaurant = dish_avg_case(false, Some(3)); + let avg_entries = + entries_of(run(&drive, &contract, &by_restaurant, false).expect("read must succeed")); + assert_eq!( + keys_of(&avg_entries), + vec!["gamma", "alpha", "beta"], + "descending by average: gamma(95) > alpha(85) > beta(60)" + ); + assert_eq!( + avg_entries[1].value, + RankedEntryValue::AvgFixedPoint(85 * RANKED_AVG_SCALE), + "alpha's two dishes average 85" + ); + + // (b) Grouping by `chefId` resolves the Count-ranked index instead — + // a different property, a different axis, a different stored + // tree type, on the same doctype and the same documents. + let by_chef = dish_count_case(false, Some(3)); + let count_entries = + entries_of(run(&drive, &contract, &by_chef, false).expect("read must succeed")); + assert_eq!( + keys_of(&count_entries), + vec!["ann", "bob", "cid"], + "descending by dish count: ann(3) > bob(2) > cid(1)" + ); + assert_eq!( + count_entries[0].value, + RankedEntryValue::Count(3), + "ann's count stays 3 despite `byChefRestaurant` hanging a \ + `restaurantId` continuation tree inside the same value tree — the \ + write path wraps it `NonCounted`, and a missing wrapper would show \ + up right here as an inflated count" + ); + assert_eq!(count_entries[1].value, RankedEntryValue::Count(2)); + assert_eq!(count_entries[2].value, RankedEntryValue::Count(1)); + + // (c) The client-side rebuild lands on the same index for the same + // request — asserted by name, then proved by the round trip: + // the verifier reads the proof against the path this index + // determines, so a divergent pick could not reconstruct the live + // root hash. + let indexes = dish_indexes(&contract); + assert_eq!( + client_side_query(&contract, &by_restaurant).index.name, + "byRestaurant" + ); + assert_eq!(client_side_query(&contract, &by_chef).index.name, "byChef"); + assert_eq!( + find_ranked_index_for_axis(indexes, GROUP_PROPERTY, RankedAxis::Avg, "grade") + .expect("the Avg ranking resolves") + .name, + "byRestaurant", + ); + assert_eq!( + find_ranked_index_for_axis(indexes, CHEF_PROPERTY, RankedAxis::Count, "") + .expect("the Count ranking resolves") + .name, + "byChef", + ); + + assert_proof_round_trips(&drive, &contract, &by_restaurant, &avg_entries); + assert_proof_round_trips(&drive, &contract, &by_chef, &count_entries); + + // (d) The same agreement under an offset. This is the sharper form + // of (c): a paginated proof binds `offset` as well as the path, + // so a client that resolved a *different* index would fail to + // reconstruct the root hash, and one that resolved the right + // index but the wrong page would fail the offset check. Both + // failure modes are live here because two ranked indexes exist. + let second_best_restaurant = dish_avg_case(false, Some(1)).at_offset(1); + let page = + page_of(run(&drive, &contract, &second_best_restaurant, false).expect("read must succeed")); + assert_eq!( + keys_of(&page.entries), + vec!["alpha"], + "gamma(95) > alpha(85) > beta(60) — rank 1 is alpha" + ); + let verified = + assert_proof_round_trips(&drive, &contract, &second_best_restaurant, &page.entries); + assert_eq!(verified.skipped, 1); + + let second_busiest_chef = dish_count_case(false, Some(1)).at_offset(1); + let page = + page_of(run(&drive, &contract, &second_busiest_chef, false).expect("read must succeed")); + assert_eq!( + keys_of(&page.entries), + vec!["bob"], + "ann(3) > bob(2) > cid(1) — rank 1 is bob" + ); + let verified = assert_proof_round_trips(&drive, &contract, &second_busiest_chef, &page.entries); + assert_eq!(verified.skipped, 1); + assert_eq!( + client_side_query(&contract, &second_busiest_chef) + .index + .name, + "byChef", + "the paginated request resolves the same index as the unpaginated one" + ); + + // The non-ranked compound index is never a candidate even though it + // leads with `chefId`, which is exactly the property this request + // groups by. `byChef` declares only the Count axis, so an Avg + // ranking over chefs has no index — and `byChefRestaurant` must not + // be press-ganged into serving it. + assert!( + find_ranked_index_for_axis(indexes, CHEF_PROPERTY, RankedAxis::Avg, "grade").is_none(), + "`byChefRestaurant` leads with chefId but is compound and unranked" + ); + let avg_by_chef = RankedCase { + group_by_property: CHEF_PROPERTY, + ..dish_avg_case(false, Some(2)) + }; + let error = run(&drive, &contract, &avg_by_chef, false) + .expect_err("ranking chefs by average grade is not served by any index"); + assert!( + matches!( + &error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(message)) + if message.contains("rankedAverageable") + ), + "the error must name the missing keyword, got: {error}" + ); +} diff --git a/packages/rs-drive/src/query/drive_document_sum_query/tests.rs b/packages/rs-drive/src/query/drive_document_sum_query/tests.rs index 37126fcbedd..1688669039b 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/tests.rs @@ -42,6 +42,9 @@ fn summable_index(name: &str, props: &[&str], summable: Option<&str>) -> Index { range_countable: false, summable: summable.map(String::from), range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, } } @@ -58,6 +61,9 @@ fn range_summable_index(name: &str, props: &[&str], summable: &str) -> Index { range_countable: false, summable: Some(summable.to_string()), range_summable: true, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, } } diff --git a/packages/rs-drive/src/query/having.rs b/packages/rs-drive/src/query/having.rs index 7a1d6f11c06..c82a3015d76 100644 --- a/packages/rs-drive/src/query/having.rs +++ b/packages/rs-drive/src/query/having.rs @@ -1,18 +1,21 @@ -//! `HAVING` clause types for the v1 `getDocuments` count surface. +//! `HAVING` clause types for the v1 `getDocuments` aggregate surface. //! -//! HAVING differs from WHERE in two structural ways the type -//! system needs to reflect: -//! - The **left** operand is a per-group aggregate (`COUNT(*)`, -//! `SUM(field)`, `AVG(field)`) rather than a raw row field. -//! - The **right** operand is either a concrete value (`> 5`, -//! `BETWEEN 5 AND 10`, `IN (5, 10, 15)`) **or** a cross-group -//! ranking (`EQ MAX`, `IN TOP(5)`, `> MIN`). The ranking -//! right-operands (`MIN` / `MAX` / `TOP(N)` / `BOTTOM(N)`) are -//! meta-aggregates computed over the set of group results, so -//! `HAVING COUNT(*) IN TOP(5)` reads as "this group's count is -//! among the five largest group counts" — a concise way to -//! express top-N/bottom-N selection without window functions or -//! `ORDER BY` + `LIMIT`. +//! `HAVING` is a **boolean predicate evaluated per group**, exactly as +//! in SQL. It differs from `WHERE` in one structural way the type +//! system needs to reflect: the left operand is a per-group aggregate +//! (`COUNT(*)`, `SUM(field)`, `AVG(field)`) rather than a raw row +//! field. The right operand is always a concrete value (`> 5`, +//! `BETWEEN 5 AND 10`, `IN (5, 10, 15)`). +//! +//! **Ranking does not live here.** "Which groups score highest on the +//! selected aggregate" is spelled with SQL's own ordering surface — +//! `ORDER BY [ASC|DESC] LIMIT n OFFSET m` — and is +//! resolved by +//! [`crate::query::drive_document_ranked_query::mode_detection`]. An +//! earlier iteration of this module carried `TOP(n)` / `BOTTOM(n)` / +//! `MIN` / `MAX` right-operands; they were removed because they +//! duplicated `ORDER BY … LIMIT` with a second, non-SQL grammar that +//! could not express an offset. //! //! The operator set matches [`crate::query::WhereOperator`] minus //! `STARTS_WITH` (prefix matching has no meaning on a scalar @@ -29,10 +32,9 @@ //! and the SDK's request builder //! (`rs-sdk/src/platform/documents/document_query.rs`) so the //! drive-side struct is the single source of truth for the shape. -//! The server currently rejects any non-empty `having` with -//! `QuerySyntaxError::Unsupported("HAVING clause is not yet -//! implemented")` — the types exist so the wire surface is stable -//! when execution lands. +//! **No part of this grammar executes today**: every non-empty +//! `having` is rejected with `QuerySyntaxError::Unsupported`. The +//! types exist so the wire surface is stable as evaluation lands. use dpp::platform_value::Value; #[cfg(feature = "serde")] @@ -42,11 +44,6 @@ use serde::{Deserialize, Serialize}; /// [`HavingClause`]. These are the per-group aggregates whose /// result is the scalar / numeric value the right-side operand /// compares against. -/// -/// `MIN` / `MAX` / `TOP` / `BOTTOM` deliberately don't appear -/// here — they're cross-group ranking primitives that live on -/// the right side via [`HavingRanking`] (e.g. -/// `HAVING COUNT(*) EQ MAX`). #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HavingAggregateFunction { @@ -72,55 +69,15 @@ pub struct HavingAggregate { pub field: String, } -/// Cross-group ranking primitive on the right side of a -/// [`HavingClause`]. The ranking is computed over the **set of -/// group results** (one per row produced by `GROUP BY`), not over -/// the raw rows — so `HAVING COUNT(*) EQ MAX` selects groups -/// whose count equals the maximum count across all groups. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum HavingRankingKind { - /// Smallest group-aggregate value across the result set - /// (single scalar). - Min, - /// Largest group-aggregate value across the result set - /// (single scalar). - Max, - /// Set of the `N` largest group-aggregate values. Pair with - /// `IN` for membership (`COUNT(*) IN TOP(5)`); single-value - /// operators (`EQ`, `>`, `<`, …) treat `TOP(1)` as the - /// maximum. - Top, - /// Set of the `N` smallest group-aggregate values. Symmetric - /// counterpart to [`Self::Top`]. - Bottom, -} - -/// Cross-group ranking operand: `kind` plus an optional `n` (only -/// meaningful for [`HavingRankingKind::Top`] / -/// [`HavingRankingKind::Bottom`]). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct HavingRanking { - /// Which ranking primitive. - pub kind: HavingRankingKind, - /// Required for `Top` / `Bottom` (1-indexed: `n=1` is the - /// single largest / smallest); must be `None` for `Min` / - /// `Max`. The wire allows it on `Min` / `Max` for forward - /// compatibility, but evaluation rejects it as a malformed - /// ranking. - pub n: Option, -} - -/// Right-side operand of a [`HavingClause`]. Either a concrete -/// value (literal scalar or list-shaped operand for -/// `BETWEEN*`/`IN`) or a cross-group ranking reference -/// ([`HavingRanking`]). +/// Right-side operand of a [`HavingClause`]: a concrete value +/// (literal scalar, or list-shaped operand for `BETWEEN*` / `IN`). /// -/// The split lives at the type level so the wire decoder rejects -/// half-built clauses ("operator says `IN`, right side is `MIN` -/// ranking with `n` set") at conversion time rather than letting -/// them reach the evaluator as ambiguous state. +/// Kept as a single-variant enum rather than collapsed into a bare +/// `Value` field on [`HavingClause`] because the wire models the +/// right operand as a `oneof`: an enum is what a `oneof` decodes +/// into, and a future right-operand kind (a correlated subquery, a +/// reference to another aggregate) lands as a variant here instead +/// of reshaping every consumer's field access. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HavingRightOperand { @@ -128,11 +85,6 @@ pub enum HavingRightOperand { /// `>=`; 2-element list `[lower, upper]` for `Between*`; /// list of candidates for `In`. Value(Value), - /// Cross-group ranking reference. Operator compatibility: - /// scalar comparison operators work with `Min` / `Max` / - /// `Top(1)` / `Bottom(1)`; `In` works with `Top(N)` / - /// `Bottom(N)` (membership in the top-N / bottom-N set). - Ranking(HavingRanking), } /// Comparison operator for a [`HavingClause`]. Mirrors @@ -140,8 +92,7 @@ pub enum HavingRightOperand { /// matching has no natural meaning against a scalar aggregate /// result, even a string-typed one). `BETWEEN*` operand semantics /// match `WhereOperator`: a 2-element list `[lower, upper]`; `IN` -/// expects a list of candidate values (or a cross-group ranking -/// set via [`HavingRightOperand::Ranking`]). +/// expects a list of candidate values. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HavingOperator { @@ -192,8 +143,7 @@ pub struct HavingClause { pub aggregate: HavingAggregate, /// Comparison operator. pub operator: HavingOperator, - /// Right-side operand — either a concrete value or a - /// cross-group ranking. See [`HavingRightOperand`] for the + /// Right-side operand. See [`HavingRightOperand`] for the /// shape contract. pub right: HavingRightOperand, } diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index 2c3669f70e3..72be0fbf86e 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -17,6 +17,16 @@ pub use { drive_document_count_query::{ CountMode, DocumentCountMode, DriveDocumentCountQuery, SplitCountEntry, }, + // Ranked-query verifier-shareable types. The verifier needs the + // whole set: `DocumentRankedMode` + `RankedPaginationInputs` to + // re-run the same versioned request validation the prover ran, + // `DriveDocumentRankedQuery` to rebuild the proved grove path, and + // `RankedEntry` / `RankedEntryValue` as the verified result shape. + drive_document_ranked_query::{ + DocumentRankedMode, DriveDocumentRankedQuery, RankedAxis, RankedEntry, RankedEntryValue, + RankedPage, RankedPaginationInputs, MAX_RANKED_LIMIT, RANKED_AVG_SCALE, + RANKED_COUNT_ORDER_KEY, + }, // Sum-query verifier-shareable types: `SumEntry` is the per-key // entry type the verifier returns, `SumMode` / `DriveDocumentSumQuery` // are shape inputs the verifier needs to rebuild the path query. @@ -24,8 +34,7 @@ pub use { drive_document_sum_query::{DriveDocumentSumQuery, SumEntry, SumMode}, grovedb::{PathQuery, Query, QueryItem, SizedQuery}, having::{ - HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRanking, - HavingRankingKind, HavingRightOperand, + HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, }, ordering::OrderClause, projection::{SelectFunction, SelectProjection}, @@ -55,6 +64,13 @@ pub use drive_document_sum_query::{ // `server`-only (parallels the sum-side server-only exports above). #[cfg(feature = "server")] pub use drive_document_average_query::{DocumentAverageRequest, DocumentAverageResponse}; + +// `DocumentRankedRequest` / `DocumentRankedResponse` are the +// server-side dispatcher ABI for the ranked surface — the types +// drive-abci's routing layer names. Server-only for the same reason +// as the count / sum / average request types above. +#[cfg(feature = "server")] +pub use drive_document_ranked_query::{DocumentRankedRequest, DocumentRankedResponse}; // Imports available when either "server" or "verify" features are enabled #[cfg(any(feature = "server", feature = "verify"))] use { @@ -224,6 +240,15 @@ pub mod drive_document_sum_query; #[cfg(any(feature = "server", feature = "verify"))] pub mod drive_document_average_query; +/// A query to rank an index's groups by a per-group aggregate — "top +/// 5 restaurants by average grade" — reading grovedb's per-axis +/// secondary Merk of an indexed tree (PR #657, PV14). Unlike the +/// count / sum / average surfaces this one never opens the value +/// trees: the ordering is maintained on write, so a ranked read is +/// `O(log n + k)` with a proof. +#[cfg(any(feature = "server", feature = "verify"))] +pub mod drive_document_ranked_query; + /// Joint count-and-sum no-prove executor surface — backs the AVG /// no-prove path's unified single-walk dispatch. See its module /// docstring for the perf / atomicity contract. Server-only because diff --git a/packages/rs-drive/src/query/projection.rs b/packages/rs-drive/src/query/projection.rs index 75dd4193c07..5efad97e114 100644 --- a/packages/rs-drive/src/query/projection.rs +++ b/packages/rs-drive/src/query/projection.rs @@ -65,13 +65,13 @@ pub enum SelectFunction { /// `MIN(field)` — smallest value of `field` in each group /// (or across all matching rows when `group_by` is empty). /// Required field. Currently always rejected with "not yet - /// implemented"; **semantically distinct** from - /// [`crate::query::HavingRankingKind::Min`] which is a - /// cross-group ranking primitive on the HAVING right side. + /// implemented". **Semantically distinct** from asking for the + /// lowest-ranked *group*, which is + /// `ORDER BY ASC LIMIT 1` over the ranked surface. Min, /// `MAX(field)` — symmetric to [`Self::Min`] for the largest /// value. Same not-yet-implemented contract; same caveat - /// versus [`crate::query::HavingRankingKind::Max`]. + /// versus `ORDER BY DESC LIMIT 1`. Max, } diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_indexed_tree/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_indexed_tree/mod.rs new file mode 100644 index 00000000000..a5c94208c36 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_indexed_tree/mod.rs @@ -0,0 +1,71 @@ +mod v0; + +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::storage_flags::StorageFlags; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::drive_versions::DriveVersion; + +impl Drive { + /// Pushes an "insert empty provable count-indexed tree" (PCIT, grovedb + /// PR 657) operation to `drive_operations`. + /// + /// The indexed primary is a byte-compatible mirror of + /// `ProvableCountTree` — every existing `AggregateCountOnRange` read + /// keeps working against it unchanged. What the variant adds is one + /// ordered secondary Merk keyed by `(count_be ‖ child_key)`, bound into + /// the parent element alongside the primary root hash, so "top / bottom K + /// groups by document count" resolves in O(log n + k) with a proof. + /// + /// Used at contract registration for an index that declares + /// `rankedCountable: true` whose range layout is count-only (no + /// `rangeSummable`). When the index also opts into the sum range axis its + /// base layout is `ProvableCountProvableSumTree`, so it takes the + /// multi-axis + /// [`Self::batch_insert_empty_provable_count_provable_sum_indexed_tree`] + /// path instead — even if Count is the only ranking axis declared. + /// + /// # Parameters + /// * `path`: The path to insert an empty tree. + /// * `key_info`: The key information of the document. + /// * `storage_flags`: Storage options for the operation. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. + pub fn batch_insert_empty_provable_count_indexed_tree<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match drive_version + .grove_methods + .batch + .batch_insert_empty_provable_count_indexed_tree + { + 0 => self.batch_insert_empty_provable_count_indexed_tree_v0( + path, + key_info, + storage_flags, + drive_operations, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_empty_provable_count_indexed_tree".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_indexed_tree/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_indexed_tree/v0/mod.rs new file mode 100644 index 00000000000..5731f7be283 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_indexed_tree/v0/mod.rs @@ -0,0 +1,58 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef, KeySize}; +use crate::util::storage_flags::StorageFlags; +use grovedb::batch::KeyInfoPath; + +impl Drive { + /// Pushes an "insert empty provable count-indexed tree" (PCIT) + /// operation to `drive_operations`. See module docs. + pub(super) fn batch_insert_empty_provable_count_indexed_tree_v0<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match key_info { + KeyRef(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_count_indexed_tree( + path_items, + key.to_vec(), + storage_flags, + ), + ); + Ok(()) + } + KeySize(key) => { + drive_operations.push( + LowLevelDriveOperation::for_estimated_path_key_empty_provable_count_indexed_tree( + KeyInfoPath::from_known_path(path), + key, + storage_flags, + ), + ); + Ok(()) + } + Key(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_count_indexed_tree( + path_items, + key, + storage_flags, + ), + ); + Ok(()) + } + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_provable_sum_indexed_tree/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_provable_sum_indexed_tree/mod.rs new file mode 100644 index 00000000000..06be28967cb --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_provable_sum_indexed_tree/mod.rs @@ -0,0 +1,75 @@ +mod v0; + +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::storage_flags::StorageFlags; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::drive_versions::DriveVersion; +use grovedb::element::IndexAxis; + +impl Drive { + /// Pushes an "insert empty provable count + provable sum indexed tree" + /// (PCPSIT, grovedb PR 657) operation to `drive_operations`. + /// + /// The multi-axis indexed variant: the primary is a byte-compatible + /// mirror of `ProvableCountProvableSumTree` (so both + /// `AggregateCountOnRange` and `AggregateSumOnRange` keep working + /// unchanged), and the parent element carries a TLV list of 1..=3 ordered + /// secondaries — one per declared ranking axis, keyed by that axis's + /// order-preserving encoding of the child's aggregate. + /// + /// This is the arm every ranked index takes except the two pure + /// single-axis shapes; in particular an index that ranks by Count alone + /// but also declares `rangeSummable` lands here, because its primary must + /// stay PCPS-shaped for the sum-on-range reads. + /// + /// # Parameters + /// * `path`: The path to insert an empty tree. + /// * `key_info`: The key information of the document. + /// * `ranked_axes`: The declared ranking axes, canonically ordered + /// (Count < Sum < Avg) and non-empty. grovedb validates the resulting + /// TLV — an empty, unsorted or duplicated list is rejected. + /// * `storage_flags`: Storage options for the operation. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(Error::GroveDB)` if `ranked_axes` is not a canonical axis list. + /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. + pub fn batch_insert_empty_provable_count_provable_sum_indexed_tree<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + ranked_axes: &[IndexAxis], + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match drive_version + .grove_methods + .batch + .batch_insert_empty_provable_count_provable_sum_indexed_tree + { + 0 => self.batch_insert_empty_provable_count_provable_sum_indexed_tree_v0( + path, + key_info, + ranked_axes, + storage_flags, + drive_operations, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_empty_provable_count_provable_sum_indexed_tree".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_provable_sum_indexed_tree/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_provable_sum_indexed_tree/v0/mod.rs new file mode 100644 index 00000000000..a49bbd74bd9 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_provable_sum_indexed_tree/v0/mod.rs @@ -0,0 +1,67 @@ +use crate::drive::document::ranked_index_tree_type::ranked_axes_tlv; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef, KeySize}; +use crate::util::storage_flags::StorageFlags; +use grovedb::batch::KeyInfoPath; +use grovedb::element::IndexAxis; + +impl Drive { + /// Pushes an "insert empty provable count + provable sum indexed tree" + /// (PCPSIT) operation to `drive_operations`. See module docs. + pub(super) fn batch_insert_empty_provable_count_provable_sum_indexed_tree_v0<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + ranked_axes: &[IndexAxis], + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + // Every secondary starts empty, so the TLV is `(tag, None)` per axis. + // grovedb re-validates canonical ordering inside the constructor. + let axes_tlv = ranked_axes_tlv(ranked_axes); + match key_info { + KeyRef(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_count_provable_sum_indexed_tree( + path_items, + key.to_vec(), + axes_tlv, + storage_flags, + )?, + ); + Ok(()) + } + KeySize(key) => { + drive_operations.push( + LowLevelDriveOperation::for_estimated_path_key_empty_provable_count_provable_sum_indexed_tree( + KeyInfoPath::from_known_path(path), + key, + axes_tlv, + storage_flags, + )?, + ); + Ok(()) + } + Key(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_count_provable_sum_indexed_tree( + path_items, + key, + axes_tlv, + storage_flags, + )?, + ); + Ok(()) + } + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_sum_indexed_tree/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_sum_indexed_tree/mod.rs new file mode 100644 index 00000000000..90052253657 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_sum_indexed_tree/mod.rs @@ -0,0 +1,67 @@ +mod v0; + +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::storage_flags::StorageFlags; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::drive_versions::DriveVersion; + +impl Drive { + /// Pushes an "insert empty provable sum-indexed tree" (PSIT, grovedb + /// PR 657) operation to `drive_operations`. + /// + /// Sum-axis mirror of + /// [`Self::batch_insert_empty_provable_count_indexed_tree`]: the primary + /// is byte-compatible with `ProvableSumTree` and the single ordered + /// secondary is keyed by `(sum_sortable_be ‖ child_key)`, so "top / + /// bottom K groups by running sum" resolves in O(log n + k) with a proof. + /// + /// Used at contract registration for an index that declares + /// `rankedSummable: true` whose range layout is sum-only (no + /// `rangeCountable`); with both range axes the index lays out as PCPS and + /// takes the multi-axis path instead. + /// + /// # Parameters + /// * `path`: The path to insert an empty tree. + /// * `key_info`: The key information of the document. + /// * `storage_flags`: Storage options for the operation. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. + pub fn batch_insert_empty_provable_sum_indexed_tree<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match drive_version + .grove_methods + .batch + .batch_insert_empty_provable_sum_indexed_tree + { + 0 => self.batch_insert_empty_provable_sum_indexed_tree_v0( + path, + key_info, + storage_flags, + drive_operations, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_empty_provable_sum_indexed_tree".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_sum_indexed_tree/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_sum_indexed_tree/v0/mod.rs new file mode 100644 index 00000000000..aa6557eef7e --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_sum_indexed_tree/v0/mod.rs @@ -0,0 +1,58 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef, KeySize}; +use crate::util::storage_flags::StorageFlags; +use grovedb::batch::KeyInfoPath; + +impl Drive { + /// Pushes an "insert empty provable sum-indexed tree" (PSIT) + /// operation to `drive_operations`. See module docs. + pub(super) fn batch_insert_empty_provable_sum_indexed_tree_v0<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match key_info { + KeyRef(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_sum_indexed_tree( + path_items, + key.to_vec(), + storage_flags, + ), + ); + Ok(()) + } + KeySize(key) => { + drive_operations.push( + LowLevelDriveOperation::for_estimated_path_key_empty_provable_sum_indexed_tree( + KeyInfoPath::from_known_path(path), + key, + storage_flags, + ), + ); + Ok(()) + } + Key(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_sum_indexed_tree( + path_items, + key, + storage_flags, + ), + ); + Ok(()) + } + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/mod.rs index 3ac6a47ee02..9216b4ab477 100644 --- a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/mod.rs @@ -10,9 +10,17 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use dpp::version::drive_versions::DriveVersion; +use grovedb::element::IndexAxis; use grovedb::{TransactionArg, TreeType}; /// How the underlying v0 body builds the empty-tree operation. +/// +/// Orthogonal to the `ranked_axes` argument every entry point also +/// passes: the mode picks *how the element is wrapped*, the axes decide +/// *which element variant is built*. Only `NotWrapped` admits a +/// non-empty axis list — an indexed tree structurally cannot be wrapped +/// (see `INDEXED_INNER_UNWRAPPABLE` in `fees::op`) — and the v0 body +/// fails closed on the other combinations rather than dropping the axes. #[derive(Clone, Copy)] enum EmptyTreeInsertMode { /// Plain empty tree of the requested type (non-aggregating parent). @@ -55,6 +63,7 @@ impl Drive { path_key_info, tree_type, EmptyTreeInsertMode::NotWrapped, // non-aggregating insert, no wrap + &[], // ranked_axes — plain (non-indexed) tree storage_flags, apply_type, transaction, @@ -70,6 +79,60 @@ impl Drive { } } + /// Ranking-aware form of [`Self::batch_insert_empty_tree_if_not_exists`] + /// for an index's **terminal property-name tree**. + /// + /// `ranked_axes` is empty for every index that declares no ranking flag, + /// in which case this is bit-identical to the plain helper — which is why + /// the index walkers can call this unconditionally without changing what + /// they emit for pre-PV14 contracts. When non-empty, `tree_type` is one of + /// the three indexed variants (grovedb PR 657) and the axes supply the TLV + /// that `TreeType` alone cannot carry. + /// + /// Shares the `batch_insert_empty_tree_if_not_exists` version slot: the + /// existence check, the pending-operation scan and the delete-cancellation + /// behaviour are the same code path, and only the element being built + /// differs. + #[allow(clippy::too_many_arguments)] + pub fn batch_insert_empty_index_tree_if_not_exists( + &self, + path_key_info: PathKeyInfo, + tree_type: TreeType, + ranked_axes: &[IndexAxis], + storage_flags: Option<&StorageFlags>, + apply_type: BatchInsertTreeApplyType, + transaction: TransactionArg, + check_existing_operations: &mut Option<&mut Vec>, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result { + match drive_version + .grove_methods + .batch + .batch_insert_empty_tree_if_not_exists + { + 0 => self.batch_insert_empty_tree_if_not_exists_v0( + path_key_info, + tree_type, + // An indexed tree can never be wrapped — see + // INDEXED_INNER_UNWRAPPABLE. + EmptyTreeInsertMode::NotWrapped, + ranked_axes, + storage_flags, + apply_type, + transaction, + check_existing_operations, + drive_operations, + drive_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_empty_index_tree_if_not_exists".to_string(), + known_versions: vec![0], + received: version, + })), + } + } + /// Pushes an "insert empty `tree_type` wrapped in the appropriate /// `Element::NonCounted` / `Element::NotSummed` / `Element::NotCountedOrSummed` /// wrapper" operation to `drive_operations`, but only if the path/key @@ -113,6 +176,7 @@ impl Drive { path_key_info, tree_type, EmptyTreeInsertMode::NonAggregatedForParent(aggregating_parent_tree_type), + &[], // ranked_axes — a wrapped child is never an indexed tree storage_flags, apply_type, transaction, @@ -151,6 +215,16 @@ impl Drive { /// would let downstream crates bypass that gate. The grove feature /// version is still dispatched like the sibling helpers so a future /// v1 of the batch-dedup semantics can't silently diverge here. + /// + /// `ranked_axes` must be empty: a ranked index's terminal + /// property-name tree is an indexed tree, which cannot be wrapped + /// *and* cannot be silently inserted unwrapped here (the + /// zero-contribution dispatcher's unwrapped fallback for sum-only + /// parents would otherwise accept one). Callers pass the resolved + /// axes rather than `&[]` so that shape fails closed with the + /// dedicated message instead of losing its secondaries — the same + /// backstop the v1 walkers got from + /// [`LowLevelDriveOperation::wrap_in_non_aggregated_for_parent_tree_type`]. #[allow(clippy::too_many_arguments)] pub(crate) fn batch_insert_empty_tree_contributing_zero_to_aggregating_parent_if_not_exists< const N: usize, @@ -159,6 +233,7 @@ impl Drive { path_key_info: PathKeyInfo, aggregating_parent_tree_type: TreeType, tree_type: TreeType, + ranked_axes: &[IndexAxis], storage_flags: Option<&StorageFlags>, apply_type: BatchInsertTreeApplyType, transaction: TransactionArg, @@ -175,6 +250,7 @@ impl Drive { path_key_info, tree_type, EmptyTreeInsertMode::ContributingZeroToParent(aggregating_parent_tree_type), + ranked_axes, storage_flags, apply_type, transaction, diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/v0/mod.rs index a601b3f4495..4e425c7296c 100644 --- a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/v0/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_tree_if_not_exists/v0/mod.rs @@ -13,6 +13,7 @@ use crate::util::storage_flags::StorageFlags; use dpp::version::drive_versions::DriveVersion; use grovedb::batch::key_info::KeyInfo; use grovedb::batch::GroveOp; +use grovedb::element::IndexAxis; use grovedb::{TransactionArg, TreeType}; impl Drive { @@ -24,6 +25,7 @@ impl Drive { path_key_info: PathKeyInfo, tree_type: TreeType, insert_mode: EmptyTreeInsertMode, + ranked_axes: &[IndexAxis], storage_flags: Option<&StorageFlags>, apply_type: BatchInsertTreeApplyType, transaction: TransactionArg, @@ -39,10 +41,37 @@ impl Drive { // only). For non-aggregating parents (`NotWrapped`), no // wrapping is needed and we fall through to the plain // empty-tree op. + // + // `ranked_axes` is non-empty only for a ranked index's terminal + // property-name tree, where `tree_type` is one of the three indexed + // variants and the element needs the axis TLV that `TreeType` cannot + // carry. It is mutually exclusive with both wrapping modes — see + // `INDEXED_INNER_UNWRAPPABLE` in `fees::op` for why an indexed tree + // can never be wrapped, and why it must not be inserted unwrapped + // under an aggregating parent either (the zero-contribution + // dispatcher's sum-only-parent fallback would otherwise take it). let build_op = |path: Vec>, key: Vec| -> Result { match insert_mode { + EmptyTreeInsertMode::NotWrapped if !ranked_axes.is_empty() => { + LowLevelDriveOperation::for_known_path_key_empty_indexed_tree( + path, + key, + tree_type, + ranked_axes, + storage_flags, + ) + } + _ if !ranked_axes.is_empty() => { + Err(Error::Drive(DriveError::NotSupported( + "a ranked index's terminal property-name tree cannot be created \ + inside an aggregating value tree: grovedb's NonCounted / NotSummed \ + / NotCountedOrSummed wrappers reject indexed inners, because an \ + indexed primary commits its aggregate and secondary root keys \ + through the very parent element the wrapper neutralizes.", + ))) + } EmptyTreeInsertMode::NotWrapped => { tree_type.empty_tree_operation_for_known_path_key(path, key, storage_flags) } @@ -353,6 +382,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -404,6 +434,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -445,6 +476,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -473,6 +505,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, None, @@ -511,6 +544,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -551,6 +585,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -591,6 +626,7 @@ mod tests { info, TreeType::NormalTree, super::EmptyTreeInsertMode::NotWrapped, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -629,12 +665,14 @@ mod tests { ) .expect("expected to insert root tree"); - // (mode, parent, inner, check on the produced element) - let cases: Vec<( + /// (insert mode, predicate on the produced element, description). + type WrappingModeCase = ( super::EmptyTreeInsertMode, fn(&Element) -> bool, &'static str, - )> = vec![ + ); + + let cases: Vec = vec![ ( super::EmptyTreeInsertMode::ContributingZeroToParent(TreeType::CountTree), |element| matches!(element, Element::NonCounted(inner) if matches!(inner.as_ref(), Element::Tree(..))), @@ -667,6 +705,7 @@ mod tests { info, TreeType::NormalTree, mode, + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -695,6 +734,7 @@ mod tests { PathKeyInfo::<0>::PathKeyRef((vec![b"root".to_vec()], b"child-sum")), TreeType::SumTree, super::EmptyTreeInsertMode::ContributingZeroToParent(TreeType::CountSumTree), + &[], None, BatchInsertTreeApplyType::StatefulBatchInsertTree, Some(&tx), @@ -719,4 +759,87 @@ mod tests { "CountSumTree parent + SumTree inner → NotCountedOrSummed(SumTree), got {element:?}" ); } + + /// The ranking axes and the wrapping mode are mutually exclusive: + /// an unwrapped insert with axes builds the indexed element, and + /// every wrapped mode with axes fails closed instead of dropping + /// the axes on the floor (which would leave a ranked index with a + /// tree whose secondaries nothing maintains). + #[test] + fn test_batch_insert_empty_tree_if_not_exists_ranked_axes() { + use crate::fees::op::LowLevelDriveOperation; + use grovedb::batch::GroveOp; + use grovedb::element::IndexAxis; + use grovedb::Element; + + let drive = setup_drive(None); + let pv = PlatformVersion::latest(); + let tx = drive.grove.start_transaction(); + + drive + .grove_insert_empty_tree( + SubtreePath::empty(), + b"root", + TreeType::NormalTree, + Some(&tx), + None, + &mut vec![], + &pv.drive, + ) + .expect("expected to insert root tree"); + + // Unwrapped + a single Count axis → the dedicated PCIT element. + let mut ops = vec![]; + let inserted = drive + .batch_insert_empty_tree_if_not_exists_v0( + PathKeyInfo::<0>::PathKeyRef((vec![b"root".to_vec()], b"ranked")), + TreeType::ProvableCountIndexedTree, + super::EmptyTreeInsertMode::NotWrapped, + &[IndexAxis::Count], + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + Some(&tx), + &mut None, + &mut ops, + &pv.drive, + ) + .expect("a ranked terminal tree under a non-aggregating parent must insert"); + assert!(inserted); + let element = match ops.pop().expect("one operation must be pushed") { + LowLevelDriveOperation::GroveOperation(grove_op) => match grove_op.op { + GroveOp::InsertOrReplace { element } => element, + other => panic!("expected InsertOrReplace, got {other:?}"), + }, + other => panic!("expected GroveOperation, got {other:?}"), + }; + assert!( + matches!(&element, Element::ProvableCountIndexedTree(..)), + "expected a ProvableCountIndexedTree element, got {element:?}" + ); + + // Both wrapping modes reject the same shape. + for mode in [ + super::EmptyTreeInsertMode::ContributingZeroToParent(TreeType::CountSumTree), + super::EmptyTreeInsertMode::NonAggregatedForParent(TreeType::CountTree), + ] { + let error = drive + .batch_insert_empty_tree_if_not_exists_v0( + PathKeyInfo::<0>::PathKeyRef((vec![b"root".to_vec()], b"ranked-wrapped")), + TreeType::ProvableCountIndexedTree, + mode, + &[IndexAxis::Count], + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + Some(&tx), + &mut None, + &mut vec![], + &pv.drive, + ) + .expect_err("a ranked terminal tree inside an aggregating value tree must fail"); + assert!( + error.to_string().contains("aggregating value tree"), + "expected the indexed-inner rejection, got: {error}" + ); + } + } } diff --git a/packages/rs-drive/src/util/grove_operations/grove_insert_empty_tree/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/grove_insert_empty_tree/v0/mod.rs index 948276443fa..0f14e32d071 100644 --- a/packages/rs-drive/src/util/grove_operations/grove_insert_empty_tree/v0/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/grove_insert_empty_tree/v0/mod.rs @@ -1,4 +1,5 @@ use crate::drive::Drive; +use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::push_drive_operation_result; @@ -38,6 +39,23 @@ impl Drive { TreeType::DenseAppendOnlyFixedSizeTree(chunk_power) => { Element::empty_dense_tree(chunk_power) } + // Single-axis indexed trees carry no axis list on the element, so + // `TreeType` fully describes them. See the matching arms in + // `fees::op`'s `empty_tree_operation_for_known_path_key`. + TreeType::ProvableSumIndexedTree => Element::empty_provable_sum_indexed_tree(), + TreeType::ProvableCountIndexedTree => Element::empty_provable_count_indexed_tree(), + // The multi-axis variant's axes TLV is not carried by `TreeType`, + // so this generic path cannot describe the `Element` to insert. + // Ranked index creation uses the axis-taking helpers instead + // (`batch_insert_empty_provable_count_provable_sum_indexed_tree`). + TreeType::ProvableCountProvableSumIndexedTree => { + return Err(Error::Drive(DriveError::NotSupported( + "grove_insert_empty_tree cannot create a \ + ProvableCountProvableSumIndexedTree — the ranked axis set is not carried \ + by TreeType; use \ + batch_insert_empty_provable_count_provable_sum_indexed_tree instead.", + ))) + } }; let cost_context = self.grove.insert( path, diff --git a/packages/rs-drive/src/util/grove_operations/mod.rs b/packages/rs-drive/src/util/grove_operations/mod.rs index ad4012a2ccf..ef3b826c99e 100644 --- a/packages/rs-drive/src/util/grove_operations/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/mod.rs @@ -106,6 +106,26 @@ pub mod batch_insert_empty_provable_count_sum_tree; /// variant once grovedb PR 670 ships) over the same tree. pub mod batch_insert_empty_provable_count_provable_sum_tree; +/// Batch insert operation into an empty provable count-**indexed** tree +/// (PCIT, grovedb PR 657). Same primary node shape as +/// [`batch_insert_empty_provable_count_tree`], plus one ordered secondary +/// Merk keyed by each child's aggregate count — the storage primitive behind +/// `rankedCountable`. +pub mod batch_insert_empty_provable_count_indexed_tree; + +/// Batch insert operation into an empty provable sum-**indexed** tree +/// (PSIT, grovedb PR 657). Sum-axis mirror of +/// [`batch_insert_empty_provable_count_indexed_tree`] — the storage primitive +/// behind `rankedSummable` on a sum-only range layout. +pub mod batch_insert_empty_provable_sum_indexed_tree; + +/// Batch insert operation into an empty provable-count + provable-sum +/// **indexed** tree (PCPSIT, grovedb PR 657). Primary mirrors +/// [`batch_insert_empty_provable_count_provable_sum_tree`]; the element +/// carries a canonical TLV of 1..=3 ordered secondaries (Count / Sum / Avg). +/// This is the arm every ranked index takes whose range layout is PCPS. +pub mod batch_insert_empty_provable_count_provable_sum_indexed_tree; + /// Batch insert operation into empty tree, but only if it doesn't already exist pub mod batch_insert_empty_tree_if_not_exists; diff --git a/packages/rs-drive/src/verify/document_ranked/mod.rs b/packages/rs-drive/src/verify/document_ranked/mod.rs new file mode 100644 index 00000000000..0a5a0e0b8a7 --- /dev/null +++ b/packages/rs-drive/src/verify/document_ranked/mod.rs @@ -0,0 +1,20 @@ +//! Verifies grovedb proofs produced by the ranked +//! (`GROUP BY … ORDER BY LIMIT n [OFFSET m]`) query surface. +//! +//! Mirrors the layering of [`super::document_count`]: a pure +//! grovedb-level verifier as a method on +//! [`DriveDocumentRankedQuery`](crate::query::DriveDocumentRankedQuery) +//! taking raw `proof: &[u8]` and returning `(RootHash, T)`. The +//! tenderdash signature composition that wraps this call lives in +//! `rs-drive-proof-verifier`. +//! +//! Only one verifier exists here, and by design: the ranked surface has a +//! single proof shape. Where the count surface has five verifiers because +//! five different grovedb primitives can answer a count, every ranked +//! request — either direction, at any offset, on any of the three axes — +//! resolves to one `prove_indexed_axis_top_k_paginated` envelope that +//! differs only in the `(axis, k, descending, offset)` tuple it echoes. + +/// Indexed-axis top-k proof verification — returns the ranked groups the +/// proof commits to, in ranking order. +pub mod verify_ranked_top_k_proof; diff --git a/packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/mod.rs b/packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/mod.rs new file mode 100644 index 00000000000..16d576a52f0 --- /dev/null +++ b/packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/mod.rs @@ -0,0 +1,52 @@ +mod v0; + +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::query::DriveDocumentRankedQuery; +use crate::verify::RootHash; +use dpp::version::PlatformVersion; + +impl DriveDocumentRankedQuery<'_> { + /// Verifies a grovedb indexed-axis paginated top-k proof and returns + /// `(root_hash, page)`. + /// + /// Counterpart to the prover-side + /// [`execute_top_k_with_proof`](Self::execute_top_k_with_proof). + /// Both sides derive the proved subtree from + /// [`indexed_property_name_tree_path`](Self::indexed_property_name_tree_path), + /// so the verifier cannot drift from the prover on *which* ranking + /// it is checking. + /// + /// The returned entries are in ranking order, exactly as the + /// unproven [`execute_top_k_no_proof`](Self::execute_top_k_no_proof) + /// would return them, and + /// [`RankedPage::skipped`](crate::query::RankedPage::skipped) + /// carries the attested starting rank so entry `i` is the group at + /// rank `skipped + i`. The caller combines `root_hash` with the + /// surrounding tenderdash signature — see `rs-drive-proof-verifier` + /// for the canonical composition. + /// + /// # Arguments + /// * `proof` — raw grovedb proof bytes. + /// * `platform_version` — selects the method version. + pub fn verify_ranked_top_k_proof( + &self, + proof: &[u8], + platform_version: &PlatformVersion, + ) -> Result<(RootHash, crate::query::RankedPage), Error> { + match platform_version + .drive + .methods + .verify + .document_ranked + .verify_ranked_top_k_proof + { + 0 => self.verify_ranked_top_k_proof_v0(proof), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "DriveDocumentRankedQuery::verify_ranked_top_k_proof".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/v0/mod.rs b/packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/v0/mod.rs new file mode 100644 index 00000000000..668ab227770 --- /dev/null +++ b/packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/v0/mod.rs @@ -0,0 +1,117 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::query::{ + DriveDocumentRankedQuery, RankedAxis, RankedEntry, RankedEntryValue, RankedPage, +}; +use crate::verify::RootHash; +use grovedb::operations::proof::indexed_axis::AxisEntries; +use grovedb::GroveDb; + +impl DriveDocumentRankedQuery<'_> { + /// v0 of [`Self::verify_ranked_top_k_proof`]. + /// + /// Rebuilds the proved subtree path with + /// [`Self::indexed_property_name_tree_path`] and hands the proof to + /// [`GroveDb::verify_indexed_axis_top_k_paginated`], which is an + /// associated function — no database handle is involved, so this + /// compiles and runs in a verifier-only build. + /// + /// Three things are checked before the page is returned: + /// + /// 1. **The envelope's `(axis, k, offset, descending)` match this + /// query.** grovedb does this itself: the values are echoed in + /// the proof and compared against the arguments, so a proof + /// generated for a different ranking — or a different page of the + /// same ranking — is rejected rather than silently reinterpreted. + /// 2. **The result's axis shape matches the requested axis** — a + /// `Count` request must not come back holding `Sum` entries. This + /// is belt-and-braces on top of (1) (the tag check already rules + /// it out) and exists so a future decoder change that decouples + /// the tag from the decoded variant surfaces as an error here + /// instead of a mis-typed number reaching the caller. + /// 3. **At most `k` entries.** Fewer is normal — the index may hold + /// fewer groups than were asked for — but more would mean the + /// proof committed a longer walk than the request authorized. + /// + /// The returned [`RankedPage::skipped`] is grovedb's independently + /// re-derived skip count, not an echo of the request: it is + /// recomputed by the verifier from the counted subtree commitments + /// in the proof bytes. It equals `self.offset` on a full page; when + /// the walk exhausted the secondary during the skip it is smaller, + /// `entries` is empty, and the pair is a proof that the ranking + /// holds exactly `skipped` groups. Deliberately **not** rejected + /// here — "you paged past the end, and here is how far the end is" + /// is a useful answer, and the only place that knows whether it is + /// acceptable is the caller. + /// + /// No `platform_version` argument: the parent dispatcher already + /// consumed it to select this version, and nothing in the body needs + /// a grove version (verification derives everything from the proof + /// bytes plus the path). + #[inline(always)] + pub(super) fn verify_ranked_top_k_proof_v0( + &self, + proof: &[u8], + ) -> Result<(RootHash, RankedPage), Error> { + let path = self.indexed_property_name_tree_path()?; + let path_refs: Vec<&[u8]> = path.iter().map(|segment| segment.as_slice()).collect(); + + let result = GroveDb::verify_indexed_axis_top_k_paginated( + proof, + path_refs.as_slice(), + self.axis.into(), + self.k, + self.offset as u64, + self.descending, + ) + .map_err(|e| Error::GroveDB(Box::new(e)))?; + + let entries = match (self.axis, result.entries) { + (RankedAxis::Count, AxisEntries::Count(entries)) => entries + .into_iter() + .map(|(count, key)| RankedEntry { + key, + value: RankedEntryValue::Count(count), + }) + .collect::>(), + (RankedAxis::Sum, AxisEntries::Sum(entries)) => entries + .into_iter() + .map(|(sum, key)| RankedEntry { + key, + value: RankedEntryValue::Sum(sum), + }) + .collect::>(), + (RankedAxis::Avg, AxisEntries::Avg(entries)) => entries + .into_iter() + .map(|(avg, key)| RankedEntry { + key, + value: RankedEntryValue::AvgFixedPoint(avg), + }) + .collect::>(), + (axis, other) => { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked top-k proof for the {axis:?} axis verified to {} entries of a \ + different axis shape", + other.len() + )))); + } + }; + + if entries.len() > self.k as usize { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked top-k proof for the {:?} axis verified to {} entries for k = {}", + self.axis, + entries.len(), + self.k + )))); + } + + Ok(( + result.root_hash, + RankedPage { + skipped: result.skipped, + entries, + }, + )) + } +} diff --git a/packages/rs-drive/src/verify/mod.rs b/packages/rs-drive/src/verify/mod.rs index a84eff8a837..8777f4b8397 100644 --- a/packages/rs-drive/src/verify/mod.rs +++ b/packages/rs-drive/src/verify/mod.rs @@ -7,6 +7,10 @@ pub mod document; /// Document-count verification methods on proofs (the /// `GetDocumentsCount` endpoint's prove-path verifiers). pub mod document_count; +/// Document-ranked verification methods on proofs (the +/// `GROUP BY … ORDER BY LIMIT n` surface's prove-path +/// verifier). +pub mod document_ranked; /// Document-sum verification methods on proofs (the /// `GetDocumentsSum` endpoint's prove-path verifiers). pub mod document_sum; diff --git a/packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json b/packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json new file mode 100644 index 00000000000..16461b952d7 --- /dev/null +++ b/packages/rs-drive/tests/supporting_files/contract/restaurants/restaurants-contract.json @@ -0,0 +1,156 @@ +{ + "$formatVersion": "0", + "id": "AY6xWncZUFv2GCrS5seqKthUfbW9yYyUXtF8diSuHQ3f", + "ownerId": "AtirhSVpAWF7dEt6dLAmesC4Sr1MsJ9bFC1nLAoNnq2S", + "version": 1, + "documentSchemas": { + "review": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurant", + "properties": [ + { "restaurantId": "asc" } + ], + "countable": "countable", + "summable": "grade", + "averageable": "grade", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "grade": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "position": 1 + } + }, + "required": [ + "restaurantId", + "grade" + ], + "additionalProperties": false + }, + "visit": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurantVisits", + "properties": [ + { "restaurantId": "asc" } + ], + "countable": "countable", + "rangeCountable": true, + "rankedCountable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "guests": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "position": 1 + } + }, + "required": [ + "restaurantId", + "guests" + ], + "additionalProperties": false + }, + "tip": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurantTips", + "properties": [ + { "restaurantId": "asc" } + ], + "summable": "amount", + "rangeSummable": true, + "rankedSummable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "amount": { + "type": "integer", + "minimum": 0, + "maximum": 1000000, + "position": 1 + } + }, + "required": [ + "restaurantId", + "amount" + ], + "additionalProperties": false + }, + "adjustment": { + "type": "object", + "documentsMutable": true, + "canBeDeleted": true, + "indices": [ + { + "name": "byRestaurantAdjustments", + "properties": [ + { "restaurantId": "asc" } + ], + "countable": "countable", + "summable": "delta", + "averageable": "delta", + "rangeCountable": true, + "rangeSummable": true, + "rangeAverageable": true, + "rankedAverageable": true + } + ], + "properties": { + "restaurantId": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "position": 0 + }, + "delta": { + "type": "integer", + "minimum": -1000, + "maximum": 1000, + "position": 1 + } + }, + "required": [ + "restaurantId", + "delta" + ], + "additionalProperties": false + } + } +} diff --git a/packages/rs-platform-version/Cargo.toml b/packages/rs-platform-version/Cargo.toml index b1be4a2bb0b..27742bc04de 100644 --- a/packages/rs-platform-version/Cargo.toml +++ b/packages/rs-platform-version/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" thiserror = { version = "2.0.12" } bincode = { version = "=2.0.1" } versioned-feature-core = { git = "https://github.com/dashpay/versioned-feature-core", version = "1.0.0" } -grovedb-version = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1" } +grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" } [features] mock-versions = [] diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index 3bc79471cd6..13825ccf8f3 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -4,6 +4,7 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; #[derive(Clone, Debug, Default)] pub struct DPPContractVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs new file mode 100644 index 00000000000..5268c041c68 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs @@ -0,0 +1,84 @@ +use crate::version::dpp_versions::dpp_contract_versions::{ + DPPContractVersions, DataContractMethodVersions, DocumentTypeClassMethodVersions, + DocumentTypeIndexVersions, DocumentTypeMethodVersions, DocumentTypeSchemaVersions, + DocumentTypeVersions, RecursiveSchemaValidatorVersions, TokenVersions, +}; +use versioned_feature_core::FeatureVersionBounds; + +// Introduced in protocol version 14. Uses the v3 document meta-schema, which +// is v2 plus the ranked index keywords (rankedCountable / rankedSummable / +// rankedAverageable). v5 (meta-schema v2) remains for protocol version 13 so +// pre-activation validation is unchanged — under v2 those keys still fail an +// index entry's `additionalProperties: false`. +// +// `try_from_schema` moves to 3, selecting a new document-type parser +// generation (`try_from_schema/v3`). New-protocol-version grammar gets its own +// generation module rather than a version gate inside a shipped one, so the +// generation-0/1/2 parsers stay byte-identical to the code consensus already +// ran and replaying a historical block cannot pick up grammar that post-dates +// it. Generation 3 admits the ranked keywords unconditionally — it exists if +// and only if the meta-schema is v3, so it needs no version read of its own. +// +// `document_type_schema` moves to 3 in the same step: generation 3 and +// meta-schema v3 are introduced together and pair by construction. Under v2 the +// ranked keys still fail an index entry's `additionalProperties: false`, so v5 +// (protocol version 13) keeps pre-activation validation unchanged. +pub const CONTRACT_VERSIONS_V6: DPPContractVersions = DPPContractVersions { + max_serialized_size: 65000, + contract_serialization_version: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 1, + }, + contract_structure_version: 1, + created_data_contract_structure: 0, + config: FeatureVersionBounds { + min_version: 1, + max_version: 1, + default_current_version: 1, + }, + methods: DataContractMethodVersions { + validate_document: 0, + validate_update: 0, + schema: 0, + validate_groups: 0, + equal_ignoring_time_fields: 0, + registration_cost: 1, + }, + document_type_versions: DocumentTypeVersions { + index_versions: DocumentTypeIndexVersions { + index_levels_from_indices: 0, + }, + class_method_versions: DocumentTypeClassMethodVersions { + try_from_schema: 3, // changed: parser generation 3 — generation 2 plus the ranked index keywords + create_document_types_from_document_schemas: 1, + }, + structure_version: 0, + schema: DocumentTypeSchemaVersions { + document_type_schema: 3, // changed: v3 document meta-schema — v2 plus the ranked index keywords, and the gate the index parser reads + should_add_creator_id: 1, + enrich_with_base_schema: 1, + find_identifier_and_binary_paths: 0, + validate_max_depth: 0, + max_depth: 256, + recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { + traversal_validator: 0, + }, + validate_schema_compatibility: 0, + }, + methods: DocumentTypeMethodVersions { + create_document_from_data: 0, + create_document_with_prevalidated_properties: 0, + prefunded_voting_balance_for_document: 0, + contested_vote_poll_for_document: 0, + estimated_size: 0, + index_for_types: 0, + max_size: 0, + serialize_value_for_key: 0, + deserialize_value_for_key: 0, + }, + }, + token_versions: TokenVersions { + validate_structure_interval: 0, + }, +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs index 27d9e905f8a..7c3e3e6dab1 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs @@ -1,5 +1,6 @@ pub mod v0; pub mod v1; +pub mod v2; use versioned_feature_core::{FeatureVersion, FeatureVersionBounds}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v2.rs new file mode 100644 index 00000000000..f70c889fcf6 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v2.rs @@ -0,0 +1,29 @@ +use crate::version::drive_abci_versions::drive_abci_query_versions::v1::DRIVE_ABCI_QUERY_VERSIONS_V1; +use crate::version::drive_abci_versions::drive_abci_query_versions::{ + DriveAbciDocumentQueryHelperVersions, DriveAbciQueryVersions, +}; + +/// Version 2 of the Drive ABCI query versions. +/// +/// Differs from v1 in exactly one slot: +/// `document_query_helpers.compute_aggregate_mode_and_check_limit` is 1 +/// rather than 0. That is the ranked-routing gate. The v0 helper has no +/// ranked path at all; the v1 helper routes a grouped aggregate whose +/// single `order_by` names the selected aggregate (`ORDER BY +/// [ASC|DESC] LIMIT n [OFFSET m]`) to the ranked executor, and otherwise +/// behaves exactly as v0. Both reject every non-empty `having` with +/// "HAVING clause is not yet implemented". +/// +/// Keeping the flip in v14's own table is what lets a mixed-version network +/// agree: protocol version 13 and earlier keep the v1 table, so those nodes +/// still reject ranked queries, while v14 nodes answer them. The wire +/// surface is unchanged — `document_query` stays at v1 because +/// `GetDocumentsRequestV1` already carries `selects` / `group_by` / +/// `having`, and the ranked *response* shape is an additive +/// `ResultData.ranked` variant older clients simply never receive. +pub const DRIVE_ABCI_QUERY_VERSIONS_V2: DriveAbciQueryVersions = DriveAbciQueryVersions { + document_query_helpers: DriveAbciDocumentQueryHelperVersions { + compute_aggregate_mode_and_check_limit: 1, + }, + ..DRIVE_ABCI_QUERY_VERSIONS_V1 +}; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs index 04b75c6761e..7b152be0f71 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs @@ -34,6 +34,13 @@ pub struct DriveDocumentQueryMethodVersions { /// Mode-detection routing table for `SELECT SUM` queries. Same /// versioning rationale as `detect_count_mode`. pub detect_sum_mode: FeatureVersion, + /// Mode-detection routing table for ranked (`HAVING ... TOP/BOTTOM/ + /// MIN/MAX`) aggregate queries served from an indexed index tree. + /// Same versioning rationale as `detect_count_mode`. Present in + /// every version table so the slot exists for older protocol + /// versions; the routing itself is unreachable before the ranked + /// contract grammar activates. + pub detect_ranked_mode: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs index fa3aa131f5f..dc32d71f710 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs @@ -17,6 +17,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = prove_document_history: 0, detect_count_mode: 0, detect_sum_mode: 0, + detect_ranked_mode: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs index 851e7281d07..6fcfc1b5dca 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs @@ -19,6 +19,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = prove_document_history: 0, detect_count_mode: 0, detect_sum_mode: 0, + detect_ranked_mode: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs index 1ce0f147b11..823842c8c01 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs @@ -29,6 +29,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V3: DriveDocumentMethodVersions = prove_document_history: 0, detect_count_mode: 0, detect_sum_mode: 0, + detect_ranked_mode: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs index 6ecd4d4f9d7..5993ebac2a9 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs @@ -5,6 +5,22 @@ use crate::version::drive_versions::drive_document_method_versions::{ DriveDocumentQueryMethodVersions, DriveDocumentUpdateMethodVersions, }; +/// V4 is protocol version 14's document-method table. It hosts two +/// independent changes that both gate at v14. +/// +/// ## 1. Contract-level ranked aggregates +/// +/// `query.detect_ranked_mode` is the routing slot for ranked +/// (`HAVING ... TOP/BOTTOM`) aggregate queries served from an indexed +/// property-name tree. The slot exists in every version table so older +/// protocol versions have a value; it is 0 here, and the ranked routing +/// itself is unreachable before the meta-schema-v3 ranked contract +/// grammar activates (which is also v14-gated, via +/// `CONTRACT_VERSIONS_V6`). Pre-v14 protocol versions therefore cannot +/// hold a ranked contract at all. +/// +/// ## 2. The shared-prefix aggregate index fix +/// /// V4 differs from V3 in four method-version bumps that fix the /// shared-prefix aggregate index defect: a contract declaring an /// aggregating (countable / summable) index terminating at a property @@ -36,6 +52,12 @@ use crate::version::drive_versions::drive_document_method_versions::{ /// describe the exact on-disk shape the insert walkers write. /// /// v1 walkers stay consensus-locked for protocol v12/v13. +/// +/// The two changes compose in the v2 walkers: the ranked upgrade decides +/// the *property-name* tree type (plain → indexed mirror), the +/// continuation demotion decides the *value* tree type, and the two +/// levels never contend. See +/// `packages/rs-drive/src/drive/document/index_level_tree_types.rs`. pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = DriveDocumentMethodVersions { query: DriveDocumentQueryMethodVersions { @@ -48,6 +70,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = prove_document_history: 0, detect_count_mode: 0, detect_sum_mode: 0, + detect_ranked_mode: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs index 5a0d08320c3..c41823323b5 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs @@ -79,6 +79,21 @@ pub struct DriveGroveBatchMethodVersions { /// `AggregateCountOnRange` / `AggregateSumOnRange` (and the /// combined variant once grovedb PR 670 ships) over a single tree. pub batch_insert_empty_provable_count_provable_sum_tree: FeatureVersion, + /// Provable count-indexed tree (grovedb PR 657). The count-only + /// ranked variant: a `ProvableCountTree`-shaped primary Merk plus a + /// single count-ordered secondary Merk, letting an index answer + /// "top k groups by count" in O(log n + k) with a proof. + pub batch_insert_empty_provable_count_indexed_tree: FeatureVersion, + /// Provable sum-indexed tree (grovedb PR 657). Sum-only ranked + /// counterpart of + /// [`Self::batch_insert_empty_provable_count_indexed_tree`]. + pub batch_insert_empty_provable_sum_indexed_tree: FeatureVersion, + /// Provable count + provable sum indexed tree (grovedb PR 657). + /// Carries a TLV list of 1..=3 ranked axes (count / sum / avg), one + /// secondary Merk each, so a single tree can be ranked on any + /// declared axis. Used whenever more than one axis is requested, or + /// when the ranked axis is `avg` (which needs both count and sum). + pub batch_insert_empty_provable_count_provable_sum_indexed_tree: FeatureVersion, pub batch_move: FeatureVersion, pub batch_insert_item_with_sum_item_if_not_exists: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs index 75d0a4f6932..acb7bae6a7a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs @@ -57,6 +57,9 @@ pub const DRIVE_GROVE_METHOD_VERSIONS_V1: DriveGroveMethodVersions = DriveGroveM batch_insert_empty_provable_sum_tree: 0, batch_insert_empty_provable_count_sum_tree: 0, batch_insert_empty_provable_count_provable_sum_tree: 0, + batch_insert_empty_provable_count_indexed_tree: 0, + batch_insert_empty_provable_sum_indexed_tree: 0, + batch_insert_empty_provable_count_provable_sum_indexed_tree: 0, batch_move: 0, batch_insert_item_with_sum_item_if_not_exists: 0, }, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs index 1ca205019a8..83e293a9c7e 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs @@ -9,6 +9,7 @@ pub struct DriveVerifyMethodVersions { pub document: DriveVerifyDocumentMethodVersions, pub document_count: DriveVerifyDocumentCountMethodVersions, pub document_sum: DriveVerifyDocumentSumMethodVersions, + pub document_ranked: DriveVerifyDocumentRankedMethodVersions, pub identity: DriveVerifyIdentityMethodVersions, pub group: DriveVerifyGroupMethodVersions, pub token: DriveVerifyTokenMethodVersions, @@ -72,6 +73,15 @@ pub struct DriveVerifyDocumentSumMethodVersions { pub verify_point_lookup_count_and_sum_proof: FeatureVersion, } +/// Versions for the ranked-aggregate (`HAVING ... TOP/BOTTOM/MIN/MAX`) +/// prove-path verifier. The single method is implemented on +/// `DriveDocumentRankedQuery` and returns `(RootHash, Vec)`, +/// delegating to grovedb's `verify_indexed_axis_top_k`. +#[derive(Clone, Debug, Default)] +pub struct DriveVerifyDocumentRankedMethodVersions { + pub verify_ranked_top_k_proof: FeatureVersion, +} + #[derive(Clone, Debug, Default)] pub struct DriveVerifyIdentityMethodVersions { pub verify_full_identities_by_public_key_hashes: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs index 7261da1950c..30e0adb14f8 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs @@ -1,8 +1,8 @@ use crate::version::drive_versions::drive_verify_method_versions::{ DriveVerifyAddressFundsMethodVersions, DriveVerifyContractMethodVersions, DriveVerifyDocumentCountMethodVersions, DriveVerifyDocumentMethodVersions, - DriveVerifyDocumentSumMethodVersions, DriveVerifyGroupMethodVersions, - DriveVerifyIdentityMethodVersions, DriveVerifyMethodVersions, + DriveVerifyDocumentRankedMethodVersions, DriveVerifyDocumentSumMethodVersions, + DriveVerifyGroupMethodVersions, DriveVerifyIdentityMethodVersions, DriveVerifyMethodVersions, DriveVerifyShieldedMethodVersions, DriveVerifySingleDocumentMethodVersions, DriveVerifyStateTransitionMethodVersions, DriveVerifySystemMethodVersions, DriveVerifyTokenMethodVersions, DriveVerifyVoteMethodVersions, @@ -39,6 +39,9 @@ pub const DRIVE_VERIFY_METHOD_VERSIONS_V1: DriveVerifyMethodVersions = DriveVeri verify_distinct_count_and_sum_proof: 0, verify_point_lookup_count_and_sum_proof: 0, }, + document_ranked: DriveVerifyDocumentRankedMethodVersions { + verify_ranked_top_k_proof: 0, + }, identity: DriveVerifyIdentityMethodVersions { verify_full_identities_by_public_key_hashes: 0, verify_full_identity_by_identity_id: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index 94159644ee5..b01afe5816d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -20,14 +20,23 @@ use crate::version::drive_versions::{ DriveProveMethodVersions, DriveSavedBlockTransactionsMethodVersions, DriveSystemEstimationCostsMethodVersions, DriveVersion, }; -use grovedb_version::version::v3::GROVE_V3; +use grovedb_version::version::v4::GROVE_V4; /// Drive version 9. -/// Introduced in protocol v14 for the shared-prefix aggregate index fix: -/// `DRIVE_DOCUMENT_METHOD_VERSIONS_V4` bumps the four index walkers to v2 -/// (and the document update walker to v1) so contracts that pair an -/// aggregating (countable / summable) index with a compound index sharing -/// its leading property can insert, update, and delete documents. +/// Introduced in protocol v14, carrying both of v14's drive-side changes: +/// +/// * **Shared-prefix aggregate index fix** — +/// `DRIVE_DOCUMENT_METHOD_VERSIONS_V4` bumps the four index walkers to v2 +/// (and the document update walker to v1) so contracts that pair an +/// aggregating (countable / summable) index with a compound index sharing +/// its leading property can insert, update, and delete documents. +/// * **Contract-level ranked aggregates** — the same V4 table adds the +/// `detect_ranked_mode` routing slot, `grove_methods.batch` gains the +/// three indexed-tree creation slots, and `methods.verify` gains +/// `document_ranked.verify_ranked_top_k_proof`. All are 0 at +/// introduction; the ranked grammar itself is gated one layer up by +/// `CONTRACT_VERSIONS_V6`'s meta schema v3. +/// /// Everything else matches `DRIVE_VERSION_V8`. pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { structure: DRIVE_STRUCTURE_V1, @@ -57,7 +66,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { remove_from_system_credits_operations: 0, calculate_total_credits_balance: 2, // ShieldedBalances root tree adds a fifth term to the equation }, - document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker — shared-prefix aggregate indexes become insertable + document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot vote: DRIVE_VOTE_METHOD_VERSIONS_V2, contract: DRIVE_CONTRACT_METHOD_VERSIONS_V3, // changed in v8: count-tree-aware contract-insertion cost estimation (v12+ countable/range_countable doctypes) fees: DriveFeesMethodVersions { calculate_fee: 0 }, @@ -133,5 +142,11 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { }, }, grove_methods: DRIVE_GROVE_METHOD_VERSIONS_V1, - grove_version: GROVE_V3, // changed in v7: upgraded for shielded transaction support + // changed in v9: GROVE_V4 activates the indexed-tree batch cleanup + // gates (overwrite inspection + delete-tree actual-type cleanup). + // Indexed trees only exist from protocol v14, so activating the + // stricter cleanup with them costs older versions nothing; staying + // on V3 would let a batch overwrite of a ranked index orphan its + // per-axis secondary storage. + grove_version: GROVE_V4, }; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index a6c307f3d41..69b0e04f66a 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1,6 +1,6 @@ use crate::version::consensus_versions::ConsensusVersions; use crate::version::dpp_versions::dpp_asset_lock_versions::v1::DPP_ASSET_LOCK_VERSIONS_V1; -use crate::version::dpp_versions::dpp_contract_versions::v5::CONTRACT_VERSIONS_V5; +use crate::version::dpp_versions::dpp_contract_versions::v6::CONTRACT_VERSIONS_V6; use crate::version::dpp_versions::dpp_costs_versions::v1::DPP_COSTS_VERSIONS_V1; use crate::version::dpp_versions::dpp_document_versions::v3::DOCUMENT_VERSIONS_V3; use crate::version::dpp_versions::dpp_factory_versions::v1::DPP_FACTORY_VERSIONS_V1; @@ -16,7 +16,7 @@ use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1; use crate::version::drive_abci_versions::drive_abci_method_versions::v9::DRIVE_ABCI_METHOD_VERSIONS_V9; -use crate::version::drive_abci_versions::drive_abci_query_versions::v1::DRIVE_ABCI_QUERY_VERSIONS_V1; +use crate::version::drive_abci_versions::drive_abci_query_versions::v2::DRIVE_ABCI_QUERY_VERSIONS_V2; use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1; use crate::version::drive_abci_versions::drive_abci_validation_versions::v9::DRIVE_ABCI_VALIDATION_VERSIONS_V9; use crate::version::drive_abci_versions::drive_abci_withdrawal_constants::v2::DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2; @@ -30,40 +30,76 @@ use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 fixes the shared-prefix aggregate index defect: a data contract -/// declaring an aggregating (countable / summable) index that terminates at -/// a property which is also the prefix of a compound index (e.g. summable -/// `[a]` next to `[a, b]`) registered successfully but rejected every -/// document insert for most flag combinations, because Drive could not -/// legally hang the compound continuation tree under the aggregating -/// per-value tree. +/// v14 hosts two consensus changes: /// -/// `DRIVE_VERSION_V9` (via `DRIVE_DOCUMENT_METHOD_VERSIONS_V4`) bumps the -/// four document index walkers (insert/delete x top-level/recursive) to v2: -/// tree types derive through a shared continuation-demotion helper (provable -/// count-bearing value trees with compound continuations demote to -/// `CountSumTree`, since grovedb rejects count-suppressed children under -/// provable count parents by design) and continuation inserts route through -/// the completed zero-contribution wrapper matrix (`NonCounted` for non-sum -/// continuations under count-sum parents, unwrapped inserts under sum-only -/// parents, and so on). No state migration is needed: shapes without -/// compound continuations produce bit-identical operations, the broken -/// shapes could never hold documents, and the one previously-insertable -/// shape the demotion changes (a provable count-bearing value tree whose -/// continuations were all sum-bearing — insertable pre-v14 only through an -/// unenforced grovedb batch guard) simply gets `CountSumTree` value trees -/// for values first seen at v14+, which readers treat identically. +/// 1. **Contract-level ranked aggregates** (this branch): an index can +/// declare that its groups are rankable by an aggregate, so a query like +/// "top 5 restaurants by average grade" is served from an ordered +/// secondary tree in O(log n + k) with a proof, instead of being rejected. +/// 2. **The shared-prefix aggregate index fix**: a data contract declaring +/// an aggregating (countable / summable) index that terminates at a +/// property which is also the prefix of a compound index (e.g. summable +/// `[a]` next to `[a, b]`) registered successfully but rejected every +/// document insert for most flag combinations, because Drive could not +/// legally hang the compound continuation tree under the aggregating +/// per-value tree. The v2 document index walkers (plus the v1 update +/// walker) that fix it gate here as well: tree types derive through a +/// shared continuation-demotion helper (provable count-bearing value +/// trees with compound continuations demote to `CountSumTree`, since +/// grovedb rejects count-suppressed children under provable count +/// parents by design) and continuation inserts route through the +/// completed zero-contribution wrapper matrix. No state migration is +/// needed: shapes without compound continuations produce bit-identical +/// operations, the broken shapes could never hold documents, and the +/// one previously-insertable shape the demotion changes (a provable +/// count-bearing value tree whose continuations were all sum-bearing — +/// insertable pre-v14 only through an unenforced grovedb batch guard) +/// simply gets `CountSumTree` value trees for values first seen at +/// v14+, which readers treat identically. /// -/// Everything else matches v13. +/// The two are orthogonal by construction: the ranked upgrade decides the +/// *property-name* tree type, the demotion decides the *value* tree type +/// one level below it, and a demoted `CountSumTree` value tree contributes +/// its (count, sum) to a ranked indexed parent exactly as the provable +/// variant did — so ranked secondaries keep ranking correctly over +/// shared-prefix shapes. +/// +/// Until a contract uses the ranked grammar, the only v14 behavior change +/// is the shared-prefix fix; everything else matches v13: +/// +/// * `CONTRACT_VERSIONS_V6` points `document_type_schema` at the v3 document +/// meta-schema, which hosts the ranked index keywords +/// (`rankedCountable` / `rankedSummable` / `rankedAverageable`). v13 keeps +/// validating against meta-schema v2, where those keys are rejected as +/// unknown properties, so a pre-v14 contract cannot smuggle them in. +/// * `DRIVE_VERSION_V9` carries `DRIVE_DOCUMENT_METHOD_VERSIONS_V4`, adding +/// the `detect_ranked_mode` routing slot, plus the grove-method slots for +/// creating the three indexed tree variants and the verify-method slot for +/// `verify_ranked_top_k_proof`. All are 0 today. The same table bumps the +/// four index walkers to v2 and the document update walker to v1 for the +/// shared-prefix fix. +/// * `DRIVE_ABCI_QUERY_VERSIONS_V2` bumps +/// `document_query_helpers.compute_aggregate_mode_and_check_limit` 0 → 1, +/// opening the ranked path on the v1 document-query handler: a grouped +/// aggregate whose single `order_by` names the selected aggregate +/// (`ORDER BY [ASC|DESC] LIMIT n [OFFSET m]`) routes to the ranked +/// executor. v13 and earlier keep the v1 table and therefore keep +/// rejecting that shape, so mixed-version networks agree across the +/// upgrade. +/// +/// The wire surface is deliberately unchanged: `GetDocumentsRequestV1` +/// already carries `selects` / `group_by` / `order_by` / `limit` / +/// `offset`; the ranked response is an additive `ResultData.ranked` +/// variant, whose `skipped` field is likewise additive. pub const PLATFORM_V14: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_14, - drive: DRIVE_VERSION_V9, // changed: v2 index walkers — shared-prefix aggregate indexes become insertable + drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V1, methods: DRIVE_ABCI_METHOD_VERSIONS_V9, validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V9, withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2, - query: DRIVE_ABCI_QUERY_VERSIONS_V1, + query: DRIVE_ABCI_QUERY_VERSIONS_V2, // changed: ranked HAVING routing gate checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, }, dpp: DPPVersion { @@ -73,7 +109,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, state_transitions: STATE_TRANSITION_VERSIONS_V3, - contract_versions: CONTRACT_VERSIONS_V5, + contract_versions: CONTRACT_VERSIONS_V6, // changed: v3 document meta-schema hosts the ranked index keywords document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, @@ -89,3 +125,135 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { tenderdash_consensus_version: 1, }, }; + +#[cfg(test)] +mod tests { + use super::*; + use crate::version::v13::PLATFORM_V13; + + /// The ranked-HAVING routing gate lives in v14's own query table, so + /// flipping it to feature version 1 touches only v14: a v13 node keeps + /// running the v0 helper, which rejects every non-empty HAVING, so a + /// mixed-version network agrees until the upgrade vote carries. + /// + /// The flip is real as of the ranked routing landing — v14 selects the + /// v1 helper, which routes a single ranking-operand HAVING clause to + /// `dispatch_ranked_v1`. A change that made v13 non-zero here would be + /// consensus-breaking for already-deployed nodes, which is exactly what + /// the v13 half of this assertion guards. + #[test] + fn ranked_having_routing_gate_is_v14_only() { + assert_eq!( + PLATFORM_V13 + .drive_abci + .query + .document_query_helpers + .compute_aggregate_mode_and_check_limit, + 0 + ); + assert_eq!( + PLATFORM_V14 + .drive_abci + .query + .document_query_helpers + .compute_aggregate_mode_and_check_limit, + 1 + ); + } + + /// The ranked index keywords are gated by the meta-schema version, so v14 + /// must select meta-schema v3 while v13 stays on v2. + #[test] + fn ranked_index_keywords_are_gated_by_meta_schema_v3() { + assert_eq!( + PLATFORM_V13 + .dpp + .contract_versions + .document_type_versions + .schema + .document_type_schema, + 2 + ); + assert_eq!( + PLATFORM_V14 + .dpp + .contract_versions + .document_type_versions + .schema + .document_type_schema, + 3 + ); + } + + /// The ranked grammar lives in its own document-type parser generation + /// rather than behind a version gate inside a shipped one, so v14 must + /// select generation 3 while v13 stays on generation 2. Pinned here + /// because it is the whole reason generations 0/1/2 can stay byte-identical + /// to what consensus already ran: a historical block replayed at v13 is + /// parsed by a generation that has never heard of the ranked keywords. + /// The grove v4 cleanup gates (batch overwrite inspection + delete-tree + /// actual-type cleanup) exist for the indexed trees that ranked indexes + /// lay down, so v14 must select grove protocol 4 while v13 stays on 3. + /// The gates are cost-neutral — they derive the old element from data the + /// merk apply already loads — and the fee-constant tests pin identical + /// fees on both sides of the boundary. Platform flows cannot themselves + /// overwrite a ranked index (the flags are immutable on contract update + /// and new indexes cannot be added to an existing document type), so the + /// cleanup behavior itself is exercised by grovedb's own overwrite suites + /// at the pinned revision; this test pins that v14 actually activates + /// them. + #[test] + fn grove_v4_cleanup_gates_activate_at_v14() { + assert_eq!(PLATFORM_V13.drive.grove_version.protocol_version, 3); + assert_eq!(PLATFORM_V14.drive.grove_version.protocol_version, 4); + } + + #[test] + fn ranked_grammar_gets_its_own_parser_generation() { + assert_eq!( + PLATFORM_V13 + .dpp + .contract_versions + .document_type_versions + .class_method_versions + .try_from_schema, + 2 + ); + assert_eq!( + PLATFORM_V14 + .dpp + .contract_versions + .document_type_versions + .class_method_versions + .try_from_schema, + 3 + ); + } + + /// v14 introduces the slots but activates none of them yet. If a later + /// change flips one of these, it must do so deliberately — and update this + /// test — rather than by inheriting a default. + #[test] + fn ranked_feature_slots_exist_but_are_dormant() { + assert_eq!( + PLATFORM_V14.drive.methods.document.query.detect_ranked_mode, + 0 + ); + assert_eq!( + PLATFORM_V14 + .drive + .methods + .verify + .document_ranked + .verify_ranked_top_k_proof, + 0 + ); + let grove = &PLATFORM_V14.drive.grove_methods.batch; + assert_eq!(grove.batch_insert_empty_provable_count_indexed_tree, 0); + assert_eq!(grove.batch_insert_empty_provable_sum_indexed_tree, 0); + assert_eq!( + grove.batch_insert_empty_provable_count_provable_sum_indexed_tree, + 0 + ); + } +} diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index 5e72b3c2deb..20dcda21499 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -50,7 +50,7 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg", zeroize = "1" # Shielded pool (optional, behind `shielded` feature) -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", optional = true } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", optional = true } # Direct `rusqlite` access so `FileBackedShieldedStore::open_path` can set # WAL + synchronous=NORMAL pragmas before handing the connection to # `ClientPersistentCommitmentTree`. Version locked to match the rev grovedb diff --git a/packages/rs-platform-wallet/src/wallet/identity/network/contact_info.rs b/packages/rs-platform-wallet/src/wallet/identity/network/contact_info.rs index 77b883a6094..36e34dd3556 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/contact_info.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/contact_info.rs @@ -126,6 +126,7 @@ impl DashPayView<'_, B> { ascending: true, }], limit: CONTACT_INFO_PAGE, + offset: None, start: start.clone(), }; diff --git a/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs b/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs index 047d4581731..b5582883845 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs @@ -748,6 +748,7 @@ fn single_profile_query( having: vec![], order_by_clauses: vec![], limit: 1, + offset: None, start: None, } } @@ -783,6 +784,7 @@ fn contact_profiles_chunk_query( ascending: true, }], limit: CONTACT_PROFILE_IN_CAP as u32, + offset: None, start: None, } } diff --git a/packages/rs-sdk-ffi/src/system/queries/path_elements.rs b/packages/rs-sdk-ffi/src/system/queries/path_elements.rs index eaedb2aaf68..0a54b9d8981 100644 --- a/packages/rs-sdk-ffi/src/system/queries/path_elements.rs +++ b/packages/rs-sdk-ffi/src/system/queries/path_elements.rs @@ -182,6 +182,21 @@ fn format_element_data(element: &Element) -> String { format!("provable_count_provable_sum_tree:{}:{}", count, sum) } Element::ProvableSumTree(_, sum, _) => format!("provable_sum_tree:{}", sum), + // Indexed trees render their aggregate exactly like the + // non-indexed Provable* tree they mirror. The secondary root + // key(s) and, for the PCPS variant, the ranked-axis TLV are + // index-maintenance detail rather than element data, so they + // stay out of this rendering the same way the primary root key + // does for every other tree variant above. + Element::ProvableSumIndexedTree(_, _, sum, _) => { + format!("provable_sum_indexed_tree:{}", sum) + } + Element::ProvableCountIndexedTree(_, _, count, _) => { + format!("provable_count_indexed_tree:{}", count) + } + Element::ProvableCountProvableSumIndexedTree(_, count, sum, _, _) => { + format!("provable_count_provable_sum_indexed_tree:{}:{}", count, sum) + } Element::CommitmentTree(_, _, _) => "commitment_tree".to_string(), Element::MmrTree(_, _) => "mmr_tree".to_string(), Element::BulkAppendTree(_, _, _) => "bulk_append_tree".to_string(), @@ -215,6 +230,11 @@ fn format_element_type(element: &Element) -> String { "provable_count_provable_sum_tree".to_string() } Element::ProvableSumTree(_, _, _) => "provable_sum_tree".to_string(), + Element::ProvableSumIndexedTree(_, _, _, _) => "provable_sum_indexed_tree".to_string(), + Element::ProvableCountIndexedTree(_, _, _, _) => "provable_count_indexed_tree".to_string(), + Element::ProvableCountProvableSumIndexedTree(_, _, _, _, _) => { + "provable_count_provable_sum_indexed_tree".to_string() + } Element::CommitmentTree(_, _, _) => "commitment_tree".to_string(), Element::MmrTree(_, _) => "mmr_tree".to_string(), Element::BulkAppendTree(_, _, _) => "bulk_append_tree".to_string(), diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 085c367aba2..8f8ba61d9df 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -18,7 +18,7 @@ drive = { path = "../rs-drive", default-features = false, features = [ ] } drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.1", features = [ +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9", features = [ "client", "sqlite", ], optional = true } diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index ff5e2a9c698..7cb0696ac20 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -714,3 +714,78 @@ impl MockResponse for drive_proof_verifier::DocumentSplitAverages { drive_proof_verifier::DocumentSplitAverages(entries) } } + +/// Wire shape for `DocumentRankedEntries` mock round-trip: the page's +/// `starting_rank`, then `(group key, axis tag, value)` triples in list +/// order — **order is the ranking**, so a map-shaped encoding (as used +/// nowhere here, but as would be the obvious alternative) would destroy +/// the answer. +/// +/// `starting_rank` is part of the encoding rather than reconstructed as +/// `0` on decode, because it is exactly what an offset test needs to +/// assert: a mock that dropped it would make every expectation look +/// like an offset-0 query and quietly pass a round-trip that lost the +/// rank base. +/// +/// The value is widened to `i128` across all three axes: `Count` +/// (`u64`) and `Sum` (`i64`) both fit losslessly, and `AvgFixedPoint` +/// is already an `i128`. One numeric column keeps the tuple flat while +/// the tag preserves which axis produced it, so a mock expectation +/// can't quietly turn a count into a sum. +type DocumentRankedPage = (u64, Vec<(Vec, u8, i128)>); + +const RANKED_TAG_COUNT: u8 = 0; +const RANKED_TAG_SUM: u8 = 1; +const RANKED_TAG_AVG: u8 = 2; + +impl MockResponse for drive_proof_verifier::DocumentRankedEntries { + fn mock_serialize(&self, _sdk: &MockDashPlatformSdk) -> Vec { + let bincode_config = standard(); + let triples: Vec<(Vec, u8, i128)> = self + .entries + .iter() + .map(|e| match e.value { + drive_proof_verifier::RankedEntryValue::Count(count) => { + (e.key.clone(), RANKED_TAG_COUNT, count as i128) + } + drive_proof_verifier::RankedEntryValue::Sum(sum) => { + (e.key.clone(), RANKED_TAG_SUM, sum as i128) + } + drive_proof_verifier::RankedEntryValue::AvgFixedPoint(avg) => { + (e.key.clone(), RANKED_TAG_AVG, avg) + } + }) + .collect(); + let page: DocumentRankedPage = (self.starting_rank, triples); + bincode::encode_to_vec(page, bincode_config).expect("encode DocumentRankedEntries") + } + + fn mock_deserialize(_sdk: &MockDashPlatformSdk, buf: &[u8]) -> Self + where + Self: Sized, + { + let bincode_config = standard(); + let ((starting_rank, triples), _): (DocumentRankedPage, _) = + bincode::decode_from_slice(buf, bincode_config).expect("decode DocumentRankedEntries"); + let entries: Vec = triples + .into_iter() + .map(|(key, tag, value)| { + let value = match tag { + RANKED_TAG_COUNT => drive_proof_verifier::RankedEntryValue::Count( + u64::try_from(value).expect("a Count entry round-trips through i128"), + ), + RANKED_TAG_SUM => drive_proof_verifier::RankedEntryValue::Sum( + i64::try_from(value).expect("a Sum entry round-trips through i128"), + ), + RANKED_TAG_AVG => drive_proof_verifier::RankedEntryValue::AvgFixedPoint(value), + other => panic!("unknown ranked axis tag {other} in mock expectation"), + }; + drive_proof_verifier::RankedEntry { key, value } + }) + .collect(); + drive_proof_verifier::DocumentRankedEntries { + starting_rank, + entries, + } + } +} diff --git a/packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs b/packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs index c17490261d3..5e4d20947ad 100644 --- a/packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs +++ b/packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs @@ -118,6 +118,7 @@ impl Sdk { ascending: true, }], limit: CONTACT_REQUEST_PAGE_SIZE, + offset: None, start: start.clone(), }; diff --git a/packages/rs-sdk/src/platform/documents/document_query.rs b/packages/rs-sdk/src/platform/documents/document_query.rs index ecab8f26daf..57648cd1988 100644 --- a/packages/rs-sdk/src/platform/documents/document_query.rs +++ b/packages/rs-sdk/src/platform/documents/document_query.rs @@ -11,12 +11,11 @@ use dapi_grpc::platform::v0::{ document_field_value, get_documents_request_v0::Start, get_documents_request_v1::{select, Select as ProtoSelect, Start as V1Start}, - having_aggregate, having_clause, having_ranking, order_clause, + having_aggregate, having_clause, order_clause, DocumentFieldValue as ProtoDocumentFieldValue, GetDocumentsRequestV0, GetDocumentsRequestV1, HavingAggregate as ProtoHavingAggregate, - HavingClause as ProtoHavingClause, HavingRanking as ProtoHavingRanking, - OrderClause as ProtoOrderClause, WhereClause as ProtoWhereClause, - WhereOperator as ProtoWhereOperator, + HavingClause as ProtoHavingClause, OrderClause as ProtoOrderClause, + WhereClause as ProtoWhereClause, WhereOperator as ProtoWhereOperator, }, GetDocumentsRequest, Proof, ResponseMetadata, }; @@ -32,10 +31,11 @@ use dpp::{ prelude::{DataContract, Identifier}, InvalidVectorSizeError, ProtocolError, }; +use drive::query::drive_document_ranked_query::mode_detection::ranked_order_key; use drive::query::{ DriveDocumentQuery, HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, - HavingRanking, HavingRankingKind, HavingRightOperand, InternalClauses, OrderClause, - SelectFunction, SelectProjection, WhereClause, WhereOperator, + HavingRightOperand, InternalClauses, OrderClause, SelectFunction, SelectProjection, + WhereClause, WhereOperator, }; use drive_proof_verifier::{types::Documents, FromProof}; @@ -80,32 +80,74 @@ pub struct DocumentQuery { /// `select=Documents` is rejected by the server as unsupported. #[cfg_attr(feature = "mocks", serde(default))] pub group_by: Vec, - /// SQL `HAVING` clauses — aggregate filters that apply to the - /// grouped rows produced by `select = Count`, `group_by = - /// […]`. Unlike `where_clauses`, the left side is an aggregate - /// (`COUNT(*)`, `SUM(field)`, `AVG(field)`, `MIN`/`MAX`, - /// `TOP`/`BOTTOM` for N-th-element selection) rather than a - /// raw row field. See [`HavingClause`] / + /// SQL `HAVING` clauses — **boolean** aggregate filters that apply + /// to the grouped rows produced by `select = Count | Sum | Avg` + /// with a non-empty `group_by`. Unlike `where_clauses`, the left + /// side is an aggregate (`COUNT(*)`, `SUM(field)`, `AVG(field)`) + /// rather than a raw row field. See [`HavingClause`] / /// [`drive::query::HavingAggregate`] / /// [`drive::query::HavingOperator`] for the catalogs. Multiple /// entries combine with implicit `AND`. /// - /// Non-empty values are rejected by the server today with + /// **Every non-empty value is rejected by the server** with /// `QuerySyntaxError::Unsupported("HAVING clause is not yet - /// implemented")` — the typed builder exists so callers can - /// encode the full aggregate-filter surface ahead of server - /// support landing without a wire-format change. + /// implemented")`, at every protocol version. The typed builder + /// exists so callers can encode `HAVING` ahead of server support + /// landing without a wire-format change. + /// + /// **`having` does not express ranking.** "The n highest-scoring + /// groups" is [`Self::order_by_selected_aggregate`] + + /// [`Self::with_limit`] — SQL's own `ORDER BY DESC LIMIT n` + /// — which *is* served, from protocol version 14. #[cfg_attr(feature = "mocks", serde(default))] pub having: Vec, - /// `order_by` clauses for the query + /// `order_by` clauses for the query. + /// + /// For `select = Documents` these order the matched rows. For the + /// **ranked** surface a single clause naming the selected + /// aggregate orders the *groups* — see + /// [`Self::order_by_selected_aggregate`], which builds it. pub order_by_clauses: Vec, /// queryset limit. `0` is the sentinel for "unset / default" and /// is translated to `None` on the V1 wire (`optional uint32`). pub limit: u32, + /// SQL `OFFSET` — how many result rows to skip before the returned + /// page. `None` leaves the field unset on the wire. + /// + /// Served on exactly one path: the **ranked** surface (protocol + /// version 14+), where it skips that many *ranks*, so + /// `.order_by_selected_aggregate(Descending).with_limit(1) + /// .with_offset(4)` is the 5th-best group. Everywhere else the + /// server rejects a set offset with `Unsupported("OFFSET + /// pagination is not yet implemented")`. + /// + /// `#[serde(default)]` for the same mock-vector compatibility + /// reason as `select` / `group_by` / `having`: a fixture captured + /// before offsets existed deserializes to `None`. + #[cfg_attr(feature = "mocks", serde(default))] + pub offset: Option, /// first object to start with pub start: Option, } +/// Which end of a ranking a +/// [`DocumentQuery::order_by_selected_aggregate`] call walks from. +/// +/// A named pair rather than a bare `ascending: bool`, because the two +/// readings of a ranked query — "the best n" and "the worst n" — are +/// what callers actually think in, and `false` meaning "best first" at +/// a call site is exactly the sort of thing that gets flipped in +/// review without anyone noticing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RankingDirection { + /// `ORDER BY DESC` — walk from the largest aggregate + /// down. The "top n" reading: entry 0 is the highest-scoring group. + Descending, + /// `ORDER BY ASC` — walk from the smallest aggregate + /// up. The "bottom n" reading: entry 0 is the lowest-scoring group. + Ascending, +} + impl DocumentQuery { /// Create new DocumentQuery for provided contract and document type name. pub fn new>>( @@ -127,6 +169,7 @@ impl DocumentQuery { having: Vec::new(), order_by_clauses: vec![], limit: 0, + offset: None, start: None, }) } @@ -244,11 +287,79 @@ impl DocumentQuery { /// implemented")`. The builder exists so SDK callers can /// encode `HAVING` ahead of server support landing without /// another version bump. + /// + /// This is **not** how you ask for a ranking — see + /// [`Self::order_by_selected_aggregate`]. pub fn with_having(mut self, having: Vec) -> Self { self.having = having; self } + /// Order the `GROUP BY` groups by the aggregate this query + /// selects — the **ranked** surface, `ORDER BY [ASC|DESC]` (protocol version 14+). + /// + /// Replaces any previously set `order_by`, because a ranked query + /// takes exactly one ordering clause and a second one is rejected + /// rather than combined. + /// + /// The ordered field name is derived from the current + /// [`Self::select`] by rs-drive's own + /// [`ranked_order_key`] — `SUM(f)` / `AVG(f)` are named by `f`, + /// and `COUNT(*)` by the `$count` sentinel. Calling + /// [`Self::with_select`] *after* this method leaves a stale field + /// name behind and the server will refuse the request; set the + /// select first, which is also how the query reads. + /// + /// Pair with [`Self::with_limit`] (the ranking's `n`, `1 ..= 100`) + /// and optionally [`Self::with_offset`], then fetch with + /// [`DocumentRankedEntries`](drive_proof_verifier::DocumentRankedEntries). + /// + /// # The 5th-best group + /// + /// ```rust,no_run + /// # use dash_sdk::platform::{DataContract, DocumentQuery}; + /// # use dash_sdk::platform::documents::document_query::RankingDirection; + /// # use dash_sdk::drive::query::SelectProjection; + /// # fn example(contract: DataContract) -> Result<(), dash_sdk::Error> { + /// // SELECT avg(grade) GROUP BY restaurantId + /// // ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4 + /// let query = DocumentQuery::new(contract, "review")? + /// .with_select(SelectProjection::avg("grade")) + /// .with_group_by("restaurantId") + /// .order_by_selected_aggregate(RankingDirection::Descending) + /// .with_limit(1) + /// .with_offset(4); + /// # Ok(()) + /// # } + /// ``` + pub fn order_by_selected_aggregate(mut self, direction: RankingDirection) -> Self { + self.order_by_clauses = vec![OrderClause { + field: ranked_order_key(&self.select).to_string(), + ascending: matches!(direction, RankingDirection::Ascending), + }]; + self + } + + /// Set the SQL `OFFSET` — how many ranks to skip before the + /// returned page. + /// + /// Only the ranked surface honours it (see + /// [`Self::order_by_selected_aggregate`]); on every other path the + /// server rejects a set offset with `Unsupported`. There is no + /// ceiling: grovedb attests the skipped region from counted + /// subtree commitments instead of walking it, so a deep offset + /// costs exactly what a shallow one does. + /// + /// An offset past the end of the ranking is a legitimate answer + /// rather than an error — the page comes back empty, and on a + /// proved fetch its `starting_rank` is the ranking's attested total + /// population. + pub fn with_offset(mut self, offset: u32) -> Self { + self.offset = Some(offset); + self + } + /// Set the query limit. `0` means "unset" — translated to /// `None` on the V1 wire (the proto field is `optional uint32`). /// @@ -373,6 +484,7 @@ impl TryFromPlatformVersioned for GetDocumentsRequest { having, order_by_clauses, limit, + offset, start, } = value; @@ -396,6 +508,7 @@ impl TryFromPlatformVersioned for GetDocumentsRequest { where_clauses, order_by_clauses, limit, + offset, start, &select, &group_by, @@ -407,6 +520,7 @@ impl TryFromPlatformVersioned for GetDocumentsRequest { where_clauses, order_by_clauses, limit, + offset, start, select, group_by, @@ -428,6 +542,7 @@ fn encode_v1( where_clauses: Vec, order_by_clauses: Vec, limit: u32, + offset: Option, start: Option, select: SelectProjection, group_by: Vec, @@ -482,10 +597,12 @@ fn encode_v1( selects: vec![select_to_proto(select)], group_by, having, - // `offset` is wire-reserved for future row-based - // pagination; the SDK doesn't surface it yet, so we - // always emit `None` here. - offset: None, + // Honoured on the ranked path (it is the `OFFSET` of + // `ORDER BY DESC LIMIT n OFFSET m`) and rejected by + // the server everywhere else. Passed straight through: + // deciding here which paths may carry an offset would put + // a second copy of that rule in the SDK. + offset, })), }) } @@ -497,6 +614,7 @@ fn encode_v0( where_clauses: Vec, order_by_clauses: Vec, limit: u32, + offset: Option, start: Option, select: &SelectProjection, group_by: &[String], @@ -523,6 +641,15 @@ fn encode_v0( .to_string(), )); } + if offset.is_some() { + // The V0 request message has no `offset` field at all, so + // silently dropping it would page from rank 0 while the caller + // believed they had skipped ahead — the one failure mode worth + // an extra branch here. + return Err(Error::Config( + "offset requires Platform v3.1+ (V1 documents wire); not supported on V0".to_string(), + )); + } // V0 carries CBOR-serialized arrays of clause components. The // server decodes them via `ciborium::de::from_reader` into a @@ -581,6 +708,7 @@ impl<'a> From<&'a DriveDocumentQuery<'a>> for DocumentQuery { let where_clauses = value.internal_clauses.clone().into(); let order_by_clauses = value.order_by.iter().map(|(_, v)| v.clone()).collect(); let limit = value.limit.unwrap_or(0) as u32; + let offset = value.offset.map(u32::from); let start = if let Some(start_at) = value.start_at { match value.start_at_included { @@ -603,6 +731,7 @@ impl<'a> From<&'a DriveDocumentQuery<'a>> for DocumentQuery { having: Vec::new(), order_by_clauses, limit, + offset, start, } } @@ -615,6 +744,7 @@ impl<'a> From> for DocumentQuery { let where_clauses = value.internal_clauses.clone().into(); let order_by_clauses = value.order_by.iter().map(|(_, v)| v.clone()).collect(); let limit = value.limit.unwrap_or(0) as u32; + let offset = value.offset.map(u32::from); let start = if let Some(start_at) = value.start_at { match value.start_at_included { @@ -637,6 +767,7 @@ impl<'a> From> for DocumentQuery { having: Vec::new(), order_by_clauses, limit, + offset, start, } } @@ -680,11 +811,30 @@ impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> { ), }; + // `DriveDocumentQuery`'s offset is a `u16`; the wire's is a + // `u32` because the ranked path takes an unbounded one. A + // documents query that overflows `u16` is refused rather than + // truncated — silently paging from a different rank than the + // caller asked for is the worst available outcome. + let offset = request + .offset + .map(|offset| { + u16::try_from(offset).map_err(|_| { + Error::Config(format!( + "offset {offset} does not fit a documents query's u16 offset \ + (max {}); offsets above that are only meaningful on the ranked \ + surface, which does not route through DriveDocumentQuery", + u16::MAX + )) + }) + }) + .transpose()?; + let query = Self { contract: &request.data_contract, document_type, internal_clauses, - offset: None, + offset, limit, order_by: request .order_by_clauses @@ -736,13 +886,11 @@ fn order_clause_to_proto(clause: OrderClause) -> ProtoOrderClause { /// counterpart. The inverse of `rs-drive-abci`'s /// `having_clause_from_proto`. Errors only on `Value` variants /// the underlying `value_to_proto` can't represent — every -/// `HavingOperator` / `HavingAggregateFunction` / -/// `HavingRankingKind` discriminant has a 1:1 wire counterpart -/// and is always convertible. +/// `HavingOperator` / `HavingAggregateFunction` discriminant has a +/// 1:1 wire counterpart and is always convertible. fn having_clause_to_proto(clause: HavingClause) -> Result { let right = match clause.right { HavingRightOperand::Value(v) => having_clause::Right::Value(value_to_proto(v)?), - HavingRightOperand::Ranking(r) => having_clause::Right::Ranking(having_ranking_to_proto(r)), }; Ok(ProtoHavingClause { aggregate: Some(having_aggregate_to_proto(clause.aggregate)), @@ -766,22 +914,6 @@ fn having_function_to_proto(function: HavingAggregateFunction) -> having_aggrega } } -fn having_ranking_to_proto(ranking: HavingRanking) -> ProtoHavingRanking { - ProtoHavingRanking { - kind: having_ranking_kind_to_proto(ranking.kind) as i32, - n: ranking.n, - } -} - -fn having_ranking_kind_to_proto(kind: HavingRankingKind) -> having_ranking::Kind { - match kind { - HavingRankingKind::Min => having_ranking::Kind::Min, - HavingRankingKind::Max => having_ranking::Kind::Max, - HavingRankingKind::Top => having_ranking::Kind::Top, - HavingRankingKind::Bottom => having_ranking::Kind::Bottom, - } -} - /// Convert a drive [`SelectProjection`] into its wire-format /// proto counterpart. Inverse of `rs-drive-abci`'s /// `select_from_proto`. Always succeeds — every diff --git a/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs b/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs new file mode 100644 index 00000000000..b0cd0d41f95 --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs @@ -0,0 +1,535 @@ +//! `FromProof` + `Fetch` for [`DocumentRankedEntries`] — the +//! **ranked** (`GROUP BY … ORDER BY LIMIT n [OFFSET m]`) +//! view of the unified `getDocuments` endpoint. +//! +//! A ranked query answers "which `n` groups score highest (or lowest) +//! on an aggregate?" — *top 5 restaurants by average grade* — in +//! `O(log n + k)`, with a proof. It reads a pre-sorted per-axis +//! *secondary* Merk maintained by the write path (grovedb PR #657) +//! rather than walking value trees, which is why it is cheap and why +//! its shape is so constrained. +//! +//! Per-request resolution (which axis, which direction, how many +//! groups, how many ranks to skip, which index covers them) lives in +//! [`super::ranked_proof_helpers`]; this module is the thin +//! `Fetch`-side wrapper. +//! +//! ## Request shape +//! +//! Exactly one aggregate `select`, exactly one `group_by` property, +//! exactly one `ORDER BY` clause naming that select's aggregate, and a +//! `LIMIT` — plus an optional `OFFSET`. No `where`, no `having`, no +//! `start_at`: each of those is rejected rather than ignored, on both +//! sides, because a ranked walk cannot honour them and silently +//! answering a different question is worse than an error. +//! +//! [`DocumentQuery::order_by_selected_aggregate`] builds the ordering +//! clause, deriving the ordered field from the `select` through +//! rs-drive's own key mapping (`SUM(f)` / `AVG(f)` are named by `f`, +//! `COUNT(*)` by the `$count` sentinel), so there is no way to name it +//! wrong by hand. +//! +//! ## Contract prerequisites +//! +//! The index must opt in with `rankedCountable` / `rankedSummable` / +//! `rankedAverageable` (meta-schema v3, **protocol version 14+**), and +//! ranked indexes are single-property. Against a protocol-version-13 +//! node the request is refused — v13's query table has no ranked path +//! and rejects the ordering as `Unsupported`. That is the intended +//! activation gate, not a bug: a v13 node and a v14 node must disagree +//! here and nowhere else, which is what lets a mixed-version network +//! run through the upgrade. +//! +//! ## Ranks, offsets, and the empty ranking +//! +//! The fetch result carries +//! [`starting_rank`](drive_proof_verifier::DocumentRankedEntries::starting_rank) +//! alongside the entries: entry `i` is the group at rank +//! `starting_rank + i`, which is what makes `LIMIT 1 OFFSET 4` +//! meaningful as "the 5th best" rather than "some entry". On the proved +//! path that number is re-derived from the proof's counted subtree +//! commitments, not taken from the node. +//! +//! An offset past the end of the ranking is a legitimate, provable +//! answer: no entries, and `starting_rank` equal to the ranking's whole +//! attested population. **Empty rankings prove too** — grovedb's +//! paginated prover emits a guaranteed-empty range against an empty +//! axis secondary rather than refusing — so querying a freshly +//! registered contract with `prove = true` returns an empty page rather +//! than an error, and the proved and unproven paths agree. +//! +//! ## Reading the values +//! +//! Entries come back in ranking order; **do not re-sort**. Averages +//! are fixed-point integers: divide by +//! [`RANKED_AVG_SCALE`](drive_proof_verifier::RANKED_AVG_SCALE) — a +//! re-export of grovedb's own constant, which moved from `10^15` to +//! `10^19` before release, so never hardcode the literal — or call +//! [`RankedEntryValue::as_f64`](drive_proof_verifier::RankedEntryValue::as_f64), +//! which does that division for you. +//! +//! **How exact the average is depends on the path.** Fetched with a +//! proof (the default, and what the examples below do), the fixed point +//! is the integer grovedb committed to and ranked on. Fetched without +//! one, the wire carries only an `f64` of the average and the SDK +//! re-scales it back, so the digits past `f64`'s ~15–16 significant +//! decimals are reconstruction noise — fine to render, not something to +//! compare for equality. Ranking *order* is exact either way. +//! +//! ## Example: top 5 restaurants by average grade +//! +//! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 5` +//! +//! ```rust,no_run +//! use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}}; +//! use dash_sdk::drive::query::SelectProjection; +//! use dash_sdk::platform::documents::document_query::RankingDirection; +//! use drive_proof_verifier::{DocumentRankedEntries, RankedEntryValue, RANKED_AVG_SCALE}; +//! use futures::executor::block_on; +//! +//! # const RESTAURANTS_CONTRACT_ID: [u8; 32] = [0; 32]; +//! let sdk = Sdk::new_mock(); +//! let contract = block_on(DataContract::fetch(&sdk, Identifier::new(RESTAURANTS_CONTRACT_ID))) +//! .expect("fetch contract") +//! .expect("contract exists"); +//! +//! let query = DocumentQuery::new(contract, "review") +//! .expect("document type exists") +//! .with_select(SelectProjection::avg("grade")) +//! .with_group_by("restaurantId") +//! .order_by_selected_aggregate(RankingDirection::Descending) +//! .with_limit(5); +//! +//! let ranked = block_on(DocumentRankedEntries::fetch(&sdk, query)) +//! .expect("fetch succeeds") +//! .expect("a well-formed ranked query always answers"); +//! +//! // Entry order IS the ranking order — best first. +//! for (offset, entry) in ranked.entries.iter().enumerate() { +//! let rank = ranked.starting_rank + offset as u64; +//! let restaurant = String::from_utf8_lossy(&entry.key); +//! if let RankedEntryValue::AvgFixedPoint(fixed_point) = entry.value { +//! // `as_f64()` is this same division, for when you only want +//! // to display the number: +//! // let average = entry.value.as_f64(); +//! // Keep the `fixed_point` itself when you need the exact +//! // integer the proof committed to — comparing two groups, +//! // reproducing the ranking, storing it. On a `prove = false` +//! // fetch that integer is a reconstruction from the wire's +//! // double, so it is only as precise as an `f64`. +//! let average = (fixed_point as f64) / (RANKED_AVG_SCALE as f64); +//! println!("#{}: {restaurant}: {average}", rank + 1); +//! } +//! } +//! ``` +//! +//! ## Example: the 5th-best restaurant +//! +//! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` +//! +//! ```rust,no_run +//! # use dash_sdk::platform::{DataContract, DocumentQuery}; +//! # use dash_sdk::platform::documents::document_query::RankingDirection; +//! # use dash_sdk::drive::query::SelectProjection; +//! # fn example(contract: DataContract) -> Result<(), dash_sdk::Error> { +//! let query = DocumentQuery::new(contract, "review")? +//! .with_select(SelectProjection::avg("grade")) +//! .with_group_by("restaurantId") +//! .order_by_selected_aggregate(RankingDirection::Descending) +//! .with_limit(1) +//! .with_offset(4); +//! # Ok(()) +//! # } +//! ``` + +use crate::platform::documents::document_query::DocumentQuery; +use crate::platform::documents::ranked_proof_helpers::verify_ranked_query; +use crate::platform::Fetch; +use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; +use dash_context_provider::ContextProvider; +use dpp::dashcore::Network; +use dpp::version::PlatformVersion; +use drive_proof_verifier::{DocumentRankedEntries, FromProof}; + +impl FromProof for DocumentRankedEntries { + type Request = DocumentQuery; + type Response = GetDocumentsResponse; + + fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( + request: I, + response: O, + _network: Network, + platform_version: &PlatformVersion, + provider: &'a dyn ContextProvider, + ) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> + where + Self: 'a, + { + let request: Self::Request = request.into(); + let response: Self::Response = response.into(); + // Unlike the count / sum / average impls there is no separate + // `assert_select_is_*` pre-check here: the ranked grammar + // check is the first step of resolution and returns the + // resolved mode, so it runs once, inside the helper. See + // `ranked_proof_helpers::assert_ranked_shape`. + let (page, mtd, proof) = + verify_ranked_query(request, response, platform_version, provider)?; + Ok((page.map(DocumentRankedEntries::from_verified), mtd, proof)) + } +} + +impl Fetch for DocumentRankedEntries { + type Query = DocumentQuery; + type Request = dapi_grpc::platform::v0::GetDocumentsRequest; +} + +#[cfg(test)] +mod tests { + //! Offline tests for the ranked client surface: the ordering + //! builder, the request→wire encoding, and the client-side grammar + //! mirror. + //! + //! Proof verification needs a populated Drive and a real grovedb + //! proof, so it is exercised where those exist: rs-drive's + //! `drive_document_ranked_query::tests` runs prover and verifier + //! against a live Drive (including a bit-flip sweep proving no + //! tamper survives with the honest root hash), and rs-drive-abci's + //! `ranked_tests` pins the wire encoding of the same values. The + //! SDK's own network-backed suites live in `tests/fetch/` and run + //! against recorded vectors; there is no ranked vector yet because + //! recording one needs a protocol-version-14 network, which does + //! not exist offline. + + use super::*; + use crate::platform::documents::document_query::RankingDirection; + use crate::platform::documents::ranked_proof_helpers::assert_ranked_shape; + use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::select as proto_select; + use dapi_grpc::platform::v0::get_documents_request::{ + order_clause, GetDocumentsRequestV1, OrderClause as ProtoOrderClause, + Version as RequestVersion, + }; + use dapi_grpc::platform::v0::GetDocumentsRequest; + use dpp::data_contract::DataContract; + use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::version::TryFromPlatformVersioned; + use drive::query::{ + HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, + SelectProjection, WhereClause, WhereOperator, RANKED_COUNT_ORDER_KEY, + }; + use std::sync::Arc; + + /// The protocol version the ranked surface activates at. Pinned + /// as a literal so a future bump of `PlatformVersion::latest()` + /// past 14 doesn't silently change what these tests encode. + fn platform_version() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + /// Any contract with a known document type — the wire encoder is + /// schema-agnostic (it never resolves `select` / `group_by` / + /// `order_by` field names against the contract; the *server* does + /// that), so the fixture's document type carries the encoding + /// test fine while the field names stay the headline example's, + /// keeping this comparable to rs-drive-abci's `ranked_tests`. + fn contract() -> Arc { + Arc::new( + get_data_contract_fixture(None, 0, platform_version().protocol_version) + .data_contract_owned(), + ) + } + + /// `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 5`. + fn top_five_by_avg_grade() -> DocumentQuery { + DocumentQuery::new(contract(), "niceDocument") + .expect("the fixture has this document type") + .with_select(SelectProjection::avg("grade")) + .with_group_by("restaurantId") + .order_by_selected_aggregate(RankingDirection::Descending) + .with_limit(5) + } + + fn encode(query: DocumentQuery) -> GetDocumentsRequest { + GetDocumentsRequest::try_from_platform_versioned(query, platform_version()) + .expect("a ranked query encodes onto the V1 wire") + } + + fn v1_of(request: GetDocumentsRequest) -> GetDocumentsRequestV1 { + match request.version.expect("the encoder always sets a version") { + RequestVersion::V1(v1) => v1, + RequestVersion::V0(_) => { + panic!("a ranked query must encode onto the V1 wire; V0 has no `order_by` targets") + } + } + } + + /// The ordered field name, asserted through the wire's `target` + /// oneof so a future change to the aggregate-target spelling can't + /// pass this test by accident. + fn ordered_field(clause: &ProtoOrderClause) -> &str { + match clause.target.as_ref().expect("the target is always set") { + order_clause::Target::Field(field) => field.as_str(), + other => panic!("a ranked ORDER BY rides the field target, got {other:?}"), + } + } + + /// The headline round-trip: the SDK must put a ranked query on the + /// wire in **exactly** the shape rs-drive-abci's + /// `avg_axis_top_k_returns_fixed_point_entries` proved the server + /// accepts. Asserted field by field rather than with a single + /// struct comparison so a failure names the field that drifted. + #[test] + fn ranked_query_encodes_the_proven_wire_shape() { + let v1 = v1_of(encode(top_five_by_avg_grade())); + + // One aggregate select, naming the averaged property. + assert_eq!(v1.selects.len(), 1, "a ranked query has exactly one select"); + assert_eq!(v1.selects[0].function, proto_select::Function::Avg as i32); + assert_eq!(v1.selects[0].field, "grade"); + + // One GROUP BY property — the ranked index's only property. + assert_eq!(v1.group_by, vec!["restaurantId".to_string()]); + + // One ORDER BY clause naming the select's aggregate, descending. + assert_eq!( + v1.order_by.len(), + 1, + "a ranked query has exactly one ordering clause — it is the ranking" + ); + assert_eq!(ordered_field(&v1.order_by[0]), "grade"); + assert!( + !v1.order_by[0].ascending, + "Descending is the `top n` reading and must not invert on the wire" + ); + + // The ranking's n rides `limit`. + assert_eq!(v1.limit, Some(5)); + + // Everything else must be at its "unset" wire value: a ranked + // request that carried any of these would be rejected. + assert!(v1.where_clauses.is_empty()); + assert!(v1.having.is_empty()); + assert_eq!(v1.offset, None); + assert!(v1.start.is_none()); + assert!(v1.prove, "the Fetch path always requests a proof"); + } + + /// **The 5th-best group.** `LIMIT 1 OFFSET 4` is the whole point of + /// the offset surface, and the offset has to survive onto the wire + /// — a dropped one silently answers "the best" instead. + #[test] + fn the_fifth_best_encodes_limit_one_offset_four() { + let query = DocumentQuery::new(contract(), "niceDocument") + .expect("the fixture has this document type") + .with_select(SelectProjection::avg("grade")) + .with_group_by("restaurantId") + .order_by_selected_aggregate(RankingDirection::Descending) + .with_limit(1) + .with_offset(4); + + let v1 = v1_of(encode(query.clone())); + assert_eq!(v1.limit, Some(1)); + assert_eq!(v1.offset, Some(4)); + assert!(!v1.order_by[0].ascending); + + // And the client-side grammar resolves it to the same page the + // prover will produce. + let mode = assert_ranked_shape(&query, platform_version()) + .expect("LIMIT 1 OFFSET 4 is well-formed"); + assert_eq!(mode.k, 1); + assert_eq!(mode.offset, 4); + assert!(mode.descending); + } + + /// An offset far past any plausible population is **not** capped: + /// grovedb attests the skipped region from counted commitments + /// rather than walking it, so a deep page costs what a shallow one + /// does and there is nothing for a cap to protect. + #[test] + fn a_very_deep_offset_is_not_capped() { + let query = top_five_by_avg_grade().with_limit(1).with_offset(u32::MAX); + assert_eq!(v1_of(encode(query.clone())).offset, Some(u32::MAX)); + let mode = assert_ranked_shape(&query, platform_version()) + .expect("an offset past the end is a provable answer, not an error"); + assert_eq!(mode.offset, u32::MAX); + } + + /// `COUNT(*)` rankings are ordered by the **`$count` sentinel**, + /// not by a property name — the axis counts documents per group, so + /// there is no property to name. The builder derives it from the + /// select through rs-drive's own mapping, which is the only way the + /// client and the server can be guaranteed to agree. + #[test] + fn count_star_ranking_orders_by_the_count_sentinel() { + let query = DocumentQuery::new(contract(), "niceDocument") + .expect("the fixture has this document type") + .with_select(SelectProjection::count_star()) + .with_group_by("restaurantId") + .order_by_selected_aggregate(RankingDirection::Descending) + .with_limit(2); + + let v1 = v1_of(encode(query)); + assert_eq!(v1.selects[0].function, proto_select::Function::Count as i32); + assert_eq!(v1.selects[0].field, ""); + assert_eq!(ordered_field(&v1.order_by[0]), RANKED_COUNT_ORDER_KEY); + assert_eq!( + RANKED_COUNT_ORDER_KEY, "$count", + "the sentinel is wire-visible; changing it is a protocol change" + ); + } + + /// `Ascending` is the "bottom n" reading and must reach the wire as + /// `ascending: true`. Pinned separately from the descending case + /// because a builder that ignored its argument would still pass the + /// headline test. + #[test] + fn ascending_encodes_the_bottom_n_reading() { + let query = top_five_by_avg_grade() + .order_by_selected_aggregate(RankingDirection::Ascending) + .with_limit(1); + let v1 = v1_of(encode(query.clone())); + assert!(v1.order_by[0].ascending); + + let mode = assert_ranked_shape(&query, platform_version()).expect("ASC LIMIT 1 is valid"); + assert!(!mode.descending, "ASC ranks lowest-first"); + assert_eq!(mode.k, 1, "ASC LIMIT 1 is the single worst-ranked group"); + } + + /// `order_by_selected_aggregate` **replaces** rather than appends. + /// A ranked query takes exactly one ordering clause, so a builder + /// that pushed would turn a second call — or a call after an + /// unrelated `with_order_by` — into a request the server rejects. + #[test] + fn order_by_selected_aggregate_replaces_any_prior_ordering() { + let query = top_five_by_avg_grade() + .with_order_by(drive::query::OrderClause { + field: "restaurantId".to_string(), + ascending: true, + }) + .order_by_selected_aggregate(RankingDirection::Descending); + + assert_eq!(query.order_by_clauses.len(), 1); + assert_eq!(query.order_by_clauses[0].field, "grade"); + assert!(assert_ranked_shape(&query, platform_version()).is_ok()); + } + + /// The client-side grammar must resolve the same + /// `(axis, descending, k, offset)` tuple the server does — that + /// tuple goes into the proof envelope and is re-checked by the + /// verifier, so a client that resolved it differently could not + /// verify an honest proof. + #[test] + fn assert_ranked_shape_resolves_the_ranking() { + let mode = assert_ranked_shape(&top_five_by_avg_grade(), platform_version()) + .expect("the headline query is well-formed"); + assert!(mode.descending, "DESC ranks highest-first"); + assert_eq!(mode.k, 5); + assert_eq!(mode.offset, 0, "an unset OFFSET is rank 0"); + assert_eq!(mode.group_by_property, "restaurantId"); + assert_eq!(mode.aggregate_field, "grade"); + } + + /// Every knob a ranked walk cannot honour is rejected **client + /// side**, before a round trip. Each of these is also rejected by + /// the server; mirroring them here turns a network error into an + /// immediate, specific one. + #[test] + fn assert_ranked_shape_rejects_what_a_ranking_cannot_honour() { + let base = top_five_by_avg_grade(); + + // An ordering on something other than the selected aggregate + // asks for an order the secondary cannot produce. + let wrong_order = { + let mut q = base.clone(); + q.order_by_clauses = vec![drive::query::OrderClause { + field: "restaurantId".to_string(), + ascending: true, + }]; + q + }; + assert!( + assert_ranked_shape(&wrong_order, platform_version()).is_err(), + "ordering by the GROUP BY property is not a ranking by the aggregate" + ); + + // No ordering at all: a plain grouped aggregate, and the caller + // wanted `DocumentSplitAverages`. + let no_order = { + let mut q = base.clone(); + q.order_by_clauses = Vec::new(); + q + }; + assert!( + assert_ranked_shape(&no_order, platform_version()).is_err(), + "a query with no ordering is not a ranked query" + ); + + let with_where = { + let mut q = base.clone(); + q.where_clauses = vec![WhereClause { + field: "restaurantId".to_string(), + operator: WhereOperator::GreaterThan, + value: dpp::platform_value::Value::Text("a".to_string()), + }]; + q + }; + let err = assert_ranked_shape(&with_where, platform_version()) + .expect_err("the axis secondary cannot rank a filtered subset"); + assert!(format!("{err}").contains("where")); + + // HAVING is a boolean per-group predicate and the ranked + // executor cannot drop groups from the middle of its walk. + let with_having = base.clone().with_having(vec![HavingClause { + aggregate: HavingAggregate { + function: HavingAggregateFunction::Avg, + field: "grade".to_string(), + }, + operator: HavingOperator::GreaterThan, + right: HavingRightOperand::Value(dpp::platform_value::Value::U64(4)), + }]); + assert!( + assert_ranked_shape(&with_having, platform_version()).is_err(), + "a ranking cannot also filter its groups" + ); + } + + /// The ranking's `n` rides `limit`, and an out-of-range one is + /// rejected rather than clamped: `k` is echoed inside the proof + /// envelope and re-checked by the verifier, so a silent clamp would + /// produce a proof the client's own reconstruction rejects. + #[test] + fn assert_ranked_shape_rejects_an_out_of_range_limit() { + // `0` is `DocumentQuery`'s "unset" sentinel, and a ranking with + // no `n` has no size; `101` is past `MAX_RANKED_LIMIT`. + for limit in [0u32, 101] { + let query = top_five_by_avg_grade().with_limit(limit); + assert!( + assert_ranked_shape(&query, platform_version()).is_err(), + "LIMIT {limit} is outside 1..=100 and must be rejected, not clamped" + ); + } + } + + /// The V0 wire has no `offset` field at all, so encoding one there + /// has to fail loudly. Dropping it would page from rank 0 while the + /// caller believed they had skipped ahead. + #[test] + fn a_v0_encode_refuses_to_silently_drop_an_offset() { + let mut v0_version = PlatformVersion::latest().clone(); + v0_version + .drive_abci + .query + .document_query + .default_current_version = 0; + + let query = DocumentQuery::new(contract(), "niceDocument") + .expect("doctype exists") + .with_offset(4); + + let err = GetDocumentsRequest::try_from_platform_versioned(query, &v0_version) + .expect_err("V0 cannot carry an offset"); + assert!( + format!("{err}").contains("offset"), + "the refusal must name the field that cannot be carried, got: {err}" + ); + } +} diff --git a/packages/rs-sdk/src/platform/documents/mod.rs b/packages/rs-sdk/src/platform/documents/mod.rs index 6463f20c658..dbb6c5ae5ba 100644 --- a/packages/rs-sdk/src/platform/documents/mod.rs +++ b/packages/rs-sdk/src/platform/documents/mod.rs @@ -6,6 +6,12 @@ pub mod document_average; pub mod document_count; pub mod document_history_query; pub mod document_query; +/// `Fetch` impl for the ranked (`GROUP BY … ORDER BY LIMIT n +/// [OFFSET m]`) result — one entry per returned group, in ranking order, +/// plus the rank the page starts at. Requires an index declaring +/// `rankedCountable` / `rankedSummable` / `rankedAverageable` +/// (protocol version 14+). +pub mod document_ranked_entries; /// `Fetch` impl for the average-side per-entry result. Mirrors /// `document_split_sums`. pub mod document_split_averages; @@ -16,5 +22,6 @@ pub mod document_split_sums; /// `Fetch` impl for the sum-side aggregate result. Mirrors /// `document_count`. Lights up alongside grovedb PR 670. pub mod document_sum; +pub(super) mod ranked_proof_helpers; pub(super) mod sum_proof_helpers; pub mod transitions; diff --git a/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs b/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs new file mode 100644 index 00000000000..9211efbce89 --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs @@ -0,0 +1,196 @@ +//! Ranked-proof dispatch used by [`DocumentRankedEntries`]. +//! +//! Ranked-side analog of [`super::count_proof_helpers`]: it turns a +//! caller-built [`DocumentQuery`] plus the node's response into a +//! verified [`RankedPage`]. The routing decisions — which ranking axis, +//! which direction, how many groups, how many ranks to skip, which +//! index covers them — are **not** re-derived here. They come from +//! rs-drive's own [`detect_ranked_mode`] and +//! [`find_ranked_index_for_axis`], the same two functions the server +//! calls, so client and server land on the same grove path and the same +//! `(axis, k, descending, offset)` tuple by construction rather than by +//! two copies of a grammar agreeing. +//! +//! Unlike the count helper there is no per-shape dispatch: the ranked +//! surface has exactly one proof primitive +//! (`prove_indexed_axis_top_k_paginated`), and all of a request's +//! variation is carried *inside* the query struct. +//! +//! [`DocumentRankedEntries`]: drive_proof_verifier::DocumentRankedEntries + +use crate::platform::documents::document_query::DocumentQuery; +use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; +use dapi_grpc::platform::VersionedGrpcResponse; +use dash_context_provider::ContextProvider; +use dpp::version::PlatformVersion; +use dpp::{ + data_contract::accessors::v0::DataContractV0Getters, + data_contract::document_type::accessors::DocumentTypeV0Getters, +}; +use drive::query::drive_document_ranked_query::index_picker::find_ranked_index_for_axis; +use drive::query::drive_document_ranked_query::mode_detection::detect_ranked_mode; +use drive::query::{ + DocumentRankedMode, DriveDocumentRankedQuery, RankedPage, RankedPaginationInputs, +}; +use drive_proof_verifier::verify_ranked_top_k_proof; + +/// Validate that the caller-built [`DocumentQuery`] really describes a +/// ranked query, and resolve it into the +/// `(axis, descending, k, offset, group property, aggregate field)` +/// tuple the index picker and the prover both work from. +/// +/// This is the ranked counterpart of +/// [`assert_select_is_count`](super::count_proof_helpers::assert_select_is_count), +/// with one deliberate difference: it *returns* the resolved mode +/// rather than returning `()`. The count assertion guards a shape the +/// verifier cannot reproduce and is therefore a separate pre-check; +/// here the very same call is the first step of resolution, and +/// running rs-drive's grammar twice (once to assert, once to resolve) +/// would be pure duplication with a chance of the two calls being +/// given different inputs. +/// +/// The grammar itself lives in rs-drive +/// ([`detect_ranked_mode`]) and is versioned through +/// `platform_version.drive.methods.document.query.detect_ranked_mode`, +/// so an SDK built against one protocol version cannot quietly accept +/// a request shape the network of that version rejects — and, more +/// importantly, cannot resolve a request to a *different* +/// `(axis, descending, k, offset)` tuple than the prover used — +/// including the `ORDER BY` clause, which *is* the ranking and so is +/// now rs-drive's to validate rather than something the SDK has to +/// reject on its own. +pub(super) fn assert_ranked_shape( + request: &DocumentQuery, + platform_version: &PlatformVersion, +) -> Result { + // `DocumentQuery` uses `0` as the "unset" sentinel for `limit` (it + // becomes `None` on the wire); `offset` is already an `Option`. + // Both are reported to rs-drive exactly as the server sees them so + // the client rejects the same requests the server would, with the + // same message. + let pagination = RankedPaginationInputs { + limit: (request.limit != 0).then_some(request.limit), + offset: request.offset, + has_start_at: request.start.is_some(), + }; + + detect_ranked_mode( + &request.select, + &request.group_by, + &request.having, + &request.order_by_clauses, + &request.where_clauses, + pagination, + platform_version, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!( + "this DocumentQuery is not a well-formed ranked query: {e}. A ranked query is \ + `.with_select()`, `.with_group_by()`, \ + `.order_by_selected_aggregate()` and `.with_limit(n)`, \ + optionally `.with_offset(m)`, with no where clauses, no having and no \ + start_at." + ), + }) +} + +/// Verify a ranked-shape proof and return the verified [`RankedPage`] — +/// the entries **in ranking order**, plus the attested rank the page +/// starts at. +/// +/// Single source of truth for the ranked proof path. The steps are +/// deliberately the same ones the drive-side suite runs in its +/// `client_side_query` helper — re-run rs-drive's versioned request +/// validation, resolve the covering index off the contract, rebuild +/// the query — because prover and verifier agreeing on the grove path +/// is what makes the proof mean anything at all. +/// +/// ## Root-hash binding +/// +/// The merk-level verifier returning `Ok` proves nothing on its own: a +/// bit-flip sweep over a real ranked envelope shows ~9% of mutations +/// verifying cleanly with the correct entries under a *different* +/// reconstructed root hash. The binding that rejects those is inside +/// [`verify_ranked_top_k_proof`], which checks the reconstructed root +/// against the quorum-signed app hash carried by the response's +/// metadata before returning. There is no path through this helper +/// that yields entries without that check having run. +pub(super) fn verify_ranked_query( + request: DocumentQuery, + response: GetDocumentsResponse, + platform_version: &PlatformVersion, + provider: &dyn ContextProvider, +) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let document_type = request + .data_contract + .document_type_for_name(&request.document_type_name) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!( + "document type {} not found in contract: {}", + request.document_type_name, e + ), + })?; + let proof = response + .proof() + .or(Err(drive_proof_verifier::Error::NoProofInResult))?; + let mtd = response + .metadata() + .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?; + + let mode = assert_ranked_shape(&request, platform_version)?; + + // Pick the index the prover picked. Availability is decided from + // the index's `ranked_*` flags, not from the stored element + // variant, and this is rs-drive's own picker — a second + // implementation here could choose a different index on a + // contract with several candidates and then verify a proof of the + // wrong subtree's ranking (or, more likely, fail to verify at all + // with a confusing merk error). + let index = find_ranked_index_for_axis( + document_type.indexes(), + &mode.group_by_property, + mode.axis, + &mode.aggregate_field, + ) + .ok_or_else(|| drive_proof_verifier::Error::RequestError { + error: format!( + "no index on document type `{}` can rank by `{:?}` grouped on `{}`: a ranked \ + query needs a single-property index over `{}` declaring `{}` (and, for SUM / \ + AVG, `summable: \"{}\"`). Ranked indexes are opt-in contract grammar \ + (meta-schema v3, protocol version 14+).", + request.document_type_name, + mode.axis, + mode.group_by_property, + mode.group_by_property, + mode.axis.required_index_keyword(), + mode.aggregate_field, + ), + })?; + + let ranked_query = DriveDocumentRankedQuery { + document_type, + contract_id: request.data_contract.id().to_buffer(), + document_type_name: request.document_type_name.clone(), + index, + axis: mode.axis, + descending: mode.descending, + k: mode.k, + offset: mode.offset, + }; + + // Binds the reconstructed grovedb root hash to the quorum-signed + // app hash before returning — see the module docs. + let (root_hash, page) = + verify_ranked_top_k_proof(&ranked_query, proof, mtd, platform_version, provider)?; + + tracing::trace!( + target: "dash_sdk::ranked_query", + root_hash = hex::encode(root_hash), + height = mtd.height, + skipped = page.skipped, + entries = page.entries.len(), + "verified ranked top-k proof" + ); + + Ok((Some(page), mtd.clone(), proof.clone())) +} diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 0c32653ad6c..4d6ba1f660f 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -433,6 +433,7 @@ impl Sdk { having: vec![], order_by_clauses: vec![], limit: 1, + offset: None, start: None, }; @@ -491,6 +492,7 @@ impl Sdk { having: vec![], order_by_clauses: vec![], limit: 1, + offset: None, start: None, }; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/queries.rs index d193df37820..499091c505a 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/queries.rs @@ -59,6 +59,7 @@ impl Sdk { having: vec![], order_by_clauses: vec![], // Remove ordering by $createdAt as it might not be indexed limit, + offset: None, start: None, }; @@ -148,6 +149,7 @@ impl Sdk { ascending: true, }], limit: limit.unwrap_or(10), + offset: None, start: None, }; diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index 1911990c8ac..d1c7099b5b7 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -281,6 +281,7 @@ impl WasmSdk { having: vec![], order_by_clauses: vec![], limit: resolve_dpns_usernames_limit(limit), + offset: None, start: None, }; diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index a8939115c7f..55865024b75 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -32,6 +32,9 @@ export type GroveElementType = | "provableCountSumTree" | "provableCountProvableSumTree" | "provableSumTree" + | "provableSumIndexedTree" + | "provableCountIndexedTree" + | "provableCountProvableSumIndexedTree" | "commitmentTree" | "mmrTree" | "bulkAppendTree" @@ -50,6 +53,9 @@ export type GroveElementType = | "nonCountedProvableCountSumTree" | "nonCountedProvableCountProvableSumTree" | "nonCountedProvableSumTree" + | "nonCountedProvableSumIndexedTree" + | "nonCountedProvableCountIndexedTree" + | "nonCountedProvableCountProvableSumIndexedTree" | "nonCountedCommitmentTree" | "nonCountedMmrTree" | "nonCountedBulkAppendTree" @@ -869,6 +875,16 @@ fn element_sum(element: &Element) -> Option { Element::ProvableCountProvableSumTree(_, _, sum, _) => Some(*sum as i128), Element::ReferenceWithSumItem(_, _, sum, _) => Some(*sum as i128), Element::ProvableSumTree(_, sum, _) => Some(*sum as i128), + // Indexed trees carry the same aggregate as their non-indexed + // counterparts — the secondary Merks are an ordering over it, not a + // second copy of it. `ProvableSumIndexedTree` is + // `(primary_root_key, secondary_root_key, sum, flags)` and + // `ProvableCountProvableSumIndexedTree` is + // `(primary_root_key, count, sum, axes, flags)`. + // `ProvableCountIndexedTree` holds a count and no sum, so it is + // deliberately absent here and falls through to `None`. + Element::ProvableSumIndexedTree(_, _, sum, _) => Some(*sum as i128), + Element::ProvableCountProvableSumIndexedTree(_, _, sum, _, _) => Some(*sum as i128), Element::NonCounted(inner) | Element::NotSummed(inner) | Element::NotCountedOrSummed(inner) => element_sum(inner), @@ -916,6 +932,11 @@ fn element_type_name(element: &Element) -> &'static str { Element::ProvableCountSumTree(_, _, _, _) => "provableCountSumTree", Element::ProvableCountProvableSumTree(_, _, _, _) => "provableCountProvableSumTree", Element::ProvableSumTree(_, _, _) => "provableSumTree", + Element::ProvableSumIndexedTree(_, _, _, _) => "provableSumIndexedTree", + Element::ProvableCountIndexedTree(_, _, _, _) => "provableCountIndexedTree", + Element::ProvableCountProvableSumIndexedTree(_, _, _, _, _) => { + "provableCountProvableSumIndexedTree" + } Element::CommitmentTree(_, _, _) => "commitmentTree", Element::MmrTree(_, _) => "mmrTree", Element::BulkAppendTree(_, _, _) => "bulkAppendTree", @@ -944,6 +965,11 @@ fn non_counted_element_type_name(element: &Element) -> &'static str { "nonCountedProvableCountProvableSumTree" } Element::ProvableSumTree(_, _, _) => "nonCountedProvableSumTree", + Element::ProvableSumIndexedTree(_, _, _, _) => "nonCountedProvableSumIndexedTree", + Element::ProvableCountIndexedTree(_, _, _, _) => "nonCountedProvableCountIndexedTree", + Element::ProvableCountProvableSumIndexedTree(_, _, _, _, _) => { + "nonCountedProvableCountProvableSumIndexedTree" + } Element::CommitmentTree(_, _, _) => "nonCountedCommitmentTree", Element::MmrTree(_, _) => "nonCountedMmrTree", Element::BulkAppendTree(_, _, _) => "nonCountedBulkAppendTree", @@ -1934,10 +1960,105 @@ mod tests { Element::NotCountedOrSummed(Box::new(Element::ProvableSumTree(None, 2, None))), "notCountedOrSummedProvableSumTree", ), + ( + Element::ProvableSumIndexedTree(None, None, 2, None), + "provableSumIndexedTree", + ), + ( + Element::ProvableCountIndexedTree(None, None, 1, None), + "provableCountIndexedTree", + ), + ( + Element::ProvableCountProvableSumIndexedTree(None, 1, 2, vec![(0, None)], None), + "provableCountProvableSumIndexedTree", + ), + ( + Element::NonCounted(Box::new(Element::ProvableSumIndexedTree( + None, None, 2, None, + ))), + "nonCountedProvableSumIndexedTree", + ), + ( + Element::NonCounted(Box::new(Element::ProvableCountIndexedTree( + None, None, 1, None, + ))), + "nonCountedProvableCountIndexedTree", + ), + ( + Element::NonCounted(Box::new(Element::ProvableCountProvableSumIndexedTree( + None, + 1, + 2, + vec![(0, None)], + None, + ))), + "nonCountedProvableCountProvableSumIndexedTree", + ), ]; + let union = typescript_element_type_union(); for (element, expected_type) in cases { assert_eq!(element_type_name(&element), expected_type); + // The hand-written `GroveElementType` union is what TypeScript + // callers narrow on, so a name the getter can emit but the union + // does not list is a silently un-narrowable value. + assert!( + union.contains(&format!("\"{expected_type}\"")), + "`{expected_type}` is emitted by the elementType getter but missing from \ + the GroveElementType TypeScript union" + ); } } + + /// The `GroveElementType` union, read back out of this file's own + /// source. `#[wasm_bindgen(typescript_custom_section)]` consumes the + /// const it is attached to on non-wasm targets, so there is no binding + /// left for a host test to name — the declaration text is the only + /// thing available, and it is the thing under test anyway. + fn typescript_element_type_union() -> String { + const DECLARATION: &str = "export type GroveElementType ="; + let source = include_str!("system.rs"); + let start = source + .find(DECLARATION) + .expect("the TypeScript custom section declares GroveElementType"); + let rest = &source[start..]; + let end = rest + .find(';') + .expect("the GroveElementType declaration is `;`-terminated"); + rest[..end].to_string() + } + + /// Indexed trees carry the same aggregate their non-indexed counterparts + /// do; the secondaries only order it. Projecting `sum` off them is what + /// keeps `pathElement.sum` meaningful once a contract declares a ranked + /// index. `ProvableCountIndexedTree` has no sum to project. + #[test] + fn should_project_the_sum_of_indexed_trees() { + assert_eq!( + element_sum(&Element::ProvableSumIndexedTree(None, None, -7, None)), + Some(-7) + ); + assert_eq!( + element_sum(&Element::ProvableCountProvableSumIndexedTree( + None, + 3, + 42, + vec![(0, None)], + None + )), + Some(42) + ); + assert_eq!( + element_sum(&Element::ProvableCountIndexedTree(None, None, 9, None)), + None, + "a count-indexed tree has a count, not a sum" + ); + assert_eq!( + element_sum(&Element::NonCounted(Box::new( + Element::ProvableSumIndexedTree(None, None, 5, None) + ))), + Some(5), + "the wrappers look through to the inner element" + ); + } }