diff --git a/Cargo.lock b/Cargo.lock index 8deb59fae54..2b169644ccf 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]] @@ -2195,6 +2195,7 @@ dependencies = [ "grovedb-costs", "grovedb-epoch-based-storage-flags", "grovedb-path", + "grovedb-query", "grovedb-storage", "grovedb-version", "hex", @@ -2232,6 +2233,7 @@ dependencies = [ "console-subscriber", "dapi-grpc", "dash-platform-macros", + "dash-platform-queries", "delegate", "derive_more 1.0.0", "dotenvy", @@ -2493,7 +2495,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]] @@ -2973,7 +2975,7 @@ dependencies = [ [[package]] name = "grovedb" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "axum 0.8.9", "bincode", @@ -3012,7 +3014,7 @@ dependencies = [ [[package]] name = "grovedb-bulk-append-tree" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "bincode", "blake3", @@ -3030,7 +3032,7 @@ dependencies = [ [[package]] name = "grovedb-commitment-tree" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "blake3", "grovedb-bulk-append-tree", @@ -3047,7 +3049,7 @@ dependencies = [ [[package]] name = "grovedb-costs" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "integer-encoding", "intmap", @@ -3057,7 +3059,7 @@ dependencies = [ [[package]] name = "grovedb-dense-fixed-sized-merkle-tree" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "bincode", "blake3", @@ -3071,7 +3073,7 @@ dependencies = [ [[package]] name = "grovedb-element" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "bincode", "bincode_derive", @@ -3087,7 +3089,7 @@ dependencies = [ [[package]] name = "grovedb-epoch-based-storage-flags" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "grovedb-costs", "hex", @@ -3099,7 +3101,7 @@ dependencies = [ [[package]] name = "grovedb-merk" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "bincode", "bincode_derive", @@ -3125,7 +3127,7 @@ dependencies = [ [[package]] name = "grovedb-merkle-mountain-range" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "bincode", "blake3", @@ -3138,7 +3140,7 @@ dependencies = [ [[package]] name = "grovedb-path" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "hex", ] @@ -3146,7 +3148,7 @@ dependencies = [ [[package]] name = "grovedb-private-document-store" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "blake3", "grovedb-bulk-append-tree", @@ -3159,7 +3161,7 @@ dependencies = [ [[package]] name = "grovedb-query" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "bincode", "byteorder", @@ -3175,7 +3177,7 @@ dependencies = [ [[package]] name = "grovedb-storage" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "blake3", "grovedb-costs", @@ -3194,7 +3196,7 @@ dependencies = [ [[package]] name = "grovedb-version" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "thiserror 2.0.18", "versioned-feature-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3203,7 +3205,7 @@ dependencies = [ [[package]] name = "grovedb-visualize" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "hex", "itertools 0.14.0", @@ -3212,7 +3214,7 @@ dependencies = [ [[package]] name = "grovedbg-types" version = "5.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=753a11f14c9a4bc72bf2d5302751dd43d174621e#753a11f14c9a4bc72bf2d5302751dd43d174621e" +source = "git+https://github.com/dashpay/grovedb?rev=6c882c3ee7d2c331f1feda2eb4223add9a6f0e45#6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" dependencies = [ "serde", "serde_with 3.21.0", @@ -3628,7 +3630,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "system-configuration", "tokio", "tower-service", @@ -3879,7 +3881,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4691,7 +4693,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]] @@ -5581,7 +5583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck 0.4.1", - "itertools 0.13.0", + "itertools 0.10.5", "log", "multimap", "petgraph", @@ -5602,7 +5604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -5615,7 +5617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -5751,7 +5753,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.5.10", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -5789,7 +5791,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.4", "tracing", "windows-sys 0.59.0", ] @@ -6612,7 +6614,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6671,7 +6673,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7531,7 +7533,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8980,7 +8982,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/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js b/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js index 0d1e656f487..e4b945e22d7 100644 --- a/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js +++ b/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js @@ -20039,6 +20039,7 @@ $root.org = (function() { * @property {number} BETWEEN_EXCLUDE_RIGHT=8 BETWEEN_EXCLUDE_RIGHT value * @property {number} IN=9 IN value * @property {number} STARTS_WITH=10 STARTS_WITH value + * @property {number} IN_TIME_RANGE=11 IN_TIME_RANGE value */ GetDocumentsRequest.WhereOperator = (function() { var valuesById = {}, values = Object.create(valuesById); @@ -20053,6 +20054,7 @@ $root.org = (function() { values[valuesById[8] = "BETWEEN_EXCLUDE_RIGHT"] = 8; values[valuesById[9] = "IN"] = 9; values[valuesById[10] = "STARTS_WITH"] = 10; + values[valuesById[11] = "IN_TIME_RANGE"] = 11; return values; })(); @@ -20870,6 +20872,7 @@ $root.org = (function() { case 8: case 9: case 10: + case 11: break; } if (message.value != null && message.hasOwnProperty("value")) { @@ -20939,6 +20942,10 @@ $root.org = (function() { case 10: message.operator = 10; break; + case "IN_TIME_RANGE": + case 11: + message.operator = 11; + break; } if (object.value != null) { if (typeof object.value !== "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 978be7d0743..f9f35bde110 100644 --- a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js +++ b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js @@ -19531,6 +19531,7 @@ $root.org = (function() { * @property {number} BETWEEN_EXCLUDE_RIGHT=8 BETWEEN_EXCLUDE_RIGHT value * @property {number} IN=9 IN value * @property {number} STARTS_WITH=10 STARTS_WITH value + * @property {number} IN_TIME_RANGE=11 IN_TIME_RANGE value */ GetDocumentsRequest.WhereOperator = (function() { var valuesById = {}, values = Object.create(valuesById); @@ -19545,6 +19546,7 @@ $root.org = (function() { values[valuesById[8] = "BETWEEN_EXCLUDE_RIGHT"] = 8; values[valuesById[9] = "IN"] = 9; values[valuesById[10] = "STARTS_WITH"] = 10; + values[valuesById[11] = "IN_TIME_RANGE"] = 11; return values; })(); @@ -20362,6 +20364,7 @@ $root.org = (function() { case 8: case 9: case 10: + case 11: break; } if (message.value != null && message.hasOwnProperty("value")) { @@ -20431,6 +20434,10 @@ $root.org = (function() { case 10: message.operator = 10; break; + case "IN_TIME_RANGE": + case 11: + message.operator = 11; + break; } if (object.value != null) { if (typeof object.value !== "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 10f2641e792..ad740f8e1c9 100644 --- a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js +++ b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js @@ -24389,7 +24389,8 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator = { BETWEEN_EXCLUDE_LEFT: 7, BETWEEN_EXCLUDE_RIGHT: 8, IN: 9, - STARTS_WITH: 10 + STARTS_WITH: 10, + IN_TIME_RANGE: 11 }; 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 c37388a2691..a713016035f 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 @@ -385,6 +385,21 @@ typedef GPB_ENUM(GetDocumentsRequest_WhereOperator) { GetDocumentsRequest_WhereOperator_BetweenExcludeRight = 8, GetDocumentsRequest_WhereOperator_In = 9, GetDocumentsRequest_WhereOperator_StartsWith = 10, + + /** + * Time-range bucket selection (v1 only; the v0 CBOR surface is + * unaffected). `field` names a timestamp covered by a `timeRange` + * index. Operand: `text` selector `"newest"`/`"oldest"` when one grid + * buckets the field, or `list` `[selector, range, step(, phase)]` in + * the contract's declared seconds to name one of several grids (zero + * phase is spelled by omission — one wire spelling per grid). The + * server resolves it to a bucket-start equality from current block + * time; the verifier re-derives the same bucket from the quorum-signed + * metadata time — an ordinary index/count proof. See `timeRange` in + * the document meta-schema and + * `drive::query::resolve_time_range_bucket_clause`. + **/ + GetDocumentsRequest_WhereOperator_InTimeRange = 11, }; GPBEnumDescriptor *GetDocumentsRequest_WhereOperator_EnumDescriptor(void); 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 dd6f5cb79a7..15343d402ec 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 @@ -5155,7 +5155,7 @@ void GetDocumentsRequest_ClearVersionOneOfCase(GetDocumentsRequest *message) { "Equal\000GreaterThan\000GreaterThanOrEquals\000Le" "ssThan\000LessThanOrEquals\000Between\000BetweenE" "xcludeBounds\000BetweenExcludeLeft\000BetweenE" - "xcludeRight\000In\000StartsWith\000"; + "xcludeRight\000In\000StartsWith\000InTimeRange\000"; static const int32_t values[] = { GetDocumentsRequest_WhereOperator_Equal, GetDocumentsRequest_WhereOperator_GreaterThan, @@ -5168,6 +5168,7 @@ void GetDocumentsRequest_ClearVersionOneOfCase(GetDocumentsRequest *message) { GetDocumentsRequest_WhereOperator_BetweenExcludeRight, GetDocumentsRequest_WhereOperator_In, GetDocumentsRequest_WhereOperator_StartsWith, + GetDocumentsRequest_WhereOperator_InTimeRange, }; GPBEnumDescriptor *worker = [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GetDocumentsRequest_WhereOperator) @@ -5196,6 +5197,7 @@ BOOL GetDocumentsRequest_WhereOperator_IsValidValue(int32_t value__) { case GetDocumentsRequest_WhereOperator_BetweenExcludeRight: case GetDocumentsRequest_WhereOperator_In: case GetDocumentsRequest_WhereOperator_StartsWith: + case GetDocumentsRequest_WhereOperator_InTimeRange: return YES; default: return NO; 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 004bbbe6f33..8481dc746ea 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\"\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' + 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\"\xf1\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\"\xfa\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\n\x12\x11\n\rIN_TIME_RANGE\x10\x0b\x42\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=65780, - serialized_end=65870, + serialized_start=65799, + serialized_end=65889, ) _sym_db.RegisterEnumDescriptor(_KEYPURPOSE) @@ -307,11 +307,16 @@ serialized_options=None, type=None, create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IN_TIME_RANGE', index=11, number=11, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, serialized_start=13436, - serialized_end=13667, + serialized_end=13686, ) _sym_db.RegisterEnumDescriptor(_GETDOCUMENTSREQUEST_WHEREOPERATOR) @@ -340,8 +345,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=28490, - serialized_end=28563, + serialized_start=28509, + serialized_end=28582, ) _sym_db.RegisterEnumDescriptor(_GETCONTESTEDRESOURCEVOTESTATEREQUEST_GETCONTESTEDRESOURCEVOTESTATEREQUESTV0_RESULTTYPE) @@ -370,8 +375,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=29485, - serialized_end=29564, + serialized_start=29504, + serialized_end=29583, ) _sym_db.RegisterEnumDescriptor(_GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0_FINISHEDVOTEINFO_FINISHEDVOTEOUTCOME) @@ -400,8 +405,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=33193, - serialized_end=33254, + serialized_start=33212, + serialized_end=33273, ) _sym_db.RegisterEnumDescriptor(_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0_RESOURCEVOTECHOICE_VOTECHOICETYPE) @@ -425,8 +430,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=51818, - serialized_end=51856, + serialized_start=51837, + serialized_end=51875, ) _sym_db.RegisterEnumDescriptor(_GETGROUPACTIONSREQUEST_ACTIONSTATUS) @@ -450,8 +455,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=53103, - serialized_end=53138, + serialized_start=53122, + serialized_end=53157, ) _sym_db.RegisterEnumDescriptor(_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT_ACTIONTYPE) @@ -475,8 +480,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=51818, - serialized_end=51856, + serialized_start=51837, + serialized_end=51875, ) _sym_db.RegisterEnumDescriptor(_GETGROUPACTIONSIGNERSREQUEST_ACTIONSTATUS) @@ -4155,7 +4160,7 @@ fields=[]), ], serialized_start=10896, - serialized_end=13678, + serialized_end=13697, ) @@ -4186,8 +4191,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14121, - serialized_end=14151, + serialized_start=14140, + serialized_end=14170, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV0 = _descriptor.Descriptor( @@ -4236,8 +4241,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=13878, - serialized_end=14161, + serialized_start=13897, + serialized_end=14180, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_DOCUMENTS = _descriptor.Descriptor( @@ -4267,8 +4272,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14121, - serialized_end=14151, + serialized_start=14140, + serialized_end=14170, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRY = _descriptor.Descriptor( @@ -4317,8 +4322,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14435, - serialized_end=14511, + serialized_start=14454, + serialized_end=14530, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTENTRIES = _descriptor.Descriptor( @@ -4348,8 +4353,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14513, - serialized_end=14627, + serialized_start=14532, + serialized_end=14646, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_COUNTRESULTS = _descriptor.Descriptor( @@ -4391,8 +4396,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14630, - serialized_end=14790, + serialized_start=14649, + serialized_end=14809, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRY = _descriptor.Descriptor( @@ -4441,8 +4446,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14792, - serialized_end=14864, + serialized_start=14811, + serialized_end=14883, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMENTRIES = _descriptor.Descriptor( @@ -4472,8 +4477,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14866, - serialized_end=14976, + serialized_start=14885, + serialized_end=14995, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_SUMRESULTS = _descriptor.Descriptor( @@ -4515,8 +4520,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14979, - serialized_end=15133, + serialized_start=14998, + serialized_end=15152, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRY = _descriptor.Descriptor( @@ -4572,8 +4577,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15135, - serialized_end=15230, + serialized_start=15154, + serialized_end=15249, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEENTRIES = _descriptor.Descriptor( @@ -4603,8 +4608,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15232, - serialized_end=15350, + serialized_start=15251, + serialized_end=15369, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGEAGGREGATE = _descriptor.Descriptor( @@ -4641,8 +4646,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15352, - serialized_end=15406, + serialized_start=15371, + serialized_end=15425, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_AVERAGERESULTS = _descriptor.Descriptor( @@ -4684,8 +4689,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15409, - serialized_end=15660, + serialized_start=15428, + serialized_end=15679, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRY = _descriptor.Descriptor( @@ -4741,8 +4746,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15662, - serialized_end=15752, + serialized_start=15681, + serialized_end=15771, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RANKEDENTRIES = _descriptor.Descriptor( @@ -4784,8 +4789,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15755, - serialized_end=15909, + serialized_start=15774, + serialized_end=15928, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1_RESULTDATA = _descriptor.Descriptor( @@ -4848,8 +4853,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=15912, - serialized_end=16451, + serialized_start=15931, + serialized_end=16470, ) _GETDOCUMENTSRESPONSE_GETDOCUMENTSRESPONSEV1 = _descriptor.Descriptor( @@ -4898,8 +4903,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=14164, - serialized_end=16461, + serialized_start=14183, + serialized_end=16480, ) _GETDOCUMENTSRESPONSE = _descriptor.Descriptor( @@ -4941,8 +4946,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=13681, - serialized_end=16472, + serialized_start=13700, + serialized_end=16491, ) @@ -5015,8 +5020,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=16601, - serialized_end=16836, + serialized_start=16620, + serialized_end=16855, ) _GETDOCUMENTHISTORYREQUEST = _descriptor.Descriptor( @@ -5051,8 +5056,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=16475, - serialized_end=16847, + serialized_start=16494, + serialized_end=16866, ) @@ -5090,8 +5095,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17253, - serialized_end=17308, + serialized_start=17272, + serialized_end=17327, ) _GETDOCUMENTHISTORYRESPONSE_GETDOCUMENTHISTORYRESPONSEV0_DOCUMENTHISTORY = _descriptor.Descriptor( @@ -5121,8 +5126,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17311, - serialized_end=17460, + serialized_start=17330, + serialized_end=17479, ) _GETDOCUMENTHISTORYRESPONSE_GETDOCUMENTHISTORYRESPONSEV0 = _descriptor.Descriptor( @@ -5171,8 +5176,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=16979, - serialized_end=17470, + serialized_start=16998, + serialized_end=17489, ) _GETDOCUMENTHISTORYRESPONSE = _descriptor.Descriptor( @@ -5207,8 +5212,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=16850, - serialized_end=17481, + serialized_start=16869, + serialized_end=17500, ) @@ -5246,8 +5251,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17633, - serialized_end=17710, + serialized_start=17652, + serialized_end=17729, ) _GETIDENTITYBYPUBLICKEYHASHREQUEST = _descriptor.Descriptor( @@ -5282,8 +5287,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17484, - serialized_end=17721, + serialized_start=17503, + serialized_end=17740, ) @@ -5333,8 +5338,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17877, - serialized_end=18059, + serialized_start=17896, + serialized_end=18078, ) _GETIDENTITYBYPUBLICKEYHASHRESPONSE = _descriptor.Descriptor( @@ -5369,8 +5374,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=17724, - serialized_end=18070, + serialized_start=17743, + serialized_end=18089, ) @@ -5420,8 +5425,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18251, - serialized_end=18379, + serialized_start=18270, + serialized_end=18398, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHREQUEST = _descriptor.Descriptor( @@ -5456,8 +5461,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18073, - serialized_end=18390, + serialized_start=18092, + serialized_end=18409, ) @@ -5493,8 +5498,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19003, - serialized_end=19057, + serialized_start=19022, + serialized_end=19076, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSE_GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSEV0_IDENTITYPROVEDRESPONSE = _descriptor.Descriptor( @@ -5536,8 +5541,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19060, - serialized_end=19226, + serialized_start=19079, + serialized_end=19245, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSE_GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSEV0 = _descriptor.Descriptor( @@ -5586,8 +5591,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18574, - serialized_end=19236, + serialized_start=18593, + serialized_end=19255, ) _GETIDENTITYBYNONUNIQUEPUBLICKEYHASHRESPONSE = _descriptor.Descriptor( @@ -5622,8 +5627,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=18393, - serialized_end=19247, + serialized_start=18412, + serialized_end=19266, ) @@ -5661,8 +5666,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19405, - serialized_end=19490, + serialized_start=19424, + serialized_end=19509, ) _WAITFORSTATETRANSITIONRESULTREQUEST = _descriptor.Descriptor( @@ -5697,8 +5702,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19250, - serialized_end=19501, + serialized_start=19269, + serialized_end=19520, ) @@ -5748,8 +5753,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19663, - serialized_end=19902, + serialized_start=19682, + serialized_end=19921, ) _WAITFORSTATETRANSITIONRESULTRESPONSE = _descriptor.Descriptor( @@ -5784,8 +5789,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19504, - serialized_end=19913, + serialized_start=19523, + serialized_end=19932, ) @@ -5823,8 +5828,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20041, - serialized_end=20101, + serialized_start=20060, + serialized_end=20120, ) _GETCONSENSUSPARAMSREQUEST = _descriptor.Descriptor( @@ -5859,8 +5864,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=19916, - serialized_end=20112, + serialized_start=19935, + serialized_end=20131, ) @@ -5905,8 +5910,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20243, - serialized_end=20323, + serialized_start=20262, + serialized_end=20342, ) _GETCONSENSUSPARAMSRESPONSE_CONSENSUSPARAMSEVIDENCE = _descriptor.Descriptor( @@ -5950,8 +5955,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20325, - serialized_end=20423, + serialized_start=20344, + serialized_end=20442, ) _GETCONSENSUSPARAMSRESPONSE_GETCONSENSUSPARAMSRESPONSEV0 = _descriptor.Descriptor( @@ -5988,8 +5993,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20426, - serialized_end=20644, + serialized_start=20445, + serialized_end=20663, ) _GETCONSENSUSPARAMSRESPONSE = _descriptor.Descriptor( @@ -6024,8 +6029,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20115, - serialized_end=20655, + serialized_start=20134, + serialized_end=20674, ) @@ -6056,8 +6061,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=20819, - serialized_end=20875, + serialized_start=20838, + serialized_end=20894, ) _GETPROTOCOLVERSIONUPGRADESTATEREQUEST = _descriptor.Descriptor( @@ -6092,8 +6097,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20658, - serialized_end=20886, + serialized_start=20677, + serialized_end=20905, ) @@ -6124,8 +6129,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=21351, - serialized_end=21501, + serialized_start=21370, + serialized_end=21520, ) _GETPROTOCOLVERSIONUPGRADESTATERESPONSE_GETPROTOCOLVERSIONUPGRADESTATERESPONSEV0_VERSIONENTRY = _descriptor.Descriptor( @@ -6162,8 +6167,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=21503, - serialized_end=21561, + serialized_start=21522, + serialized_end=21580, ) _GETPROTOCOLVERSIONUPGRADESTATERESPONSE_GETPROTOCOLVERSIONUPGRADESTATERESPONSEV0 = _descriptor.Descriptor( @@ -6212,8 +6217,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=21054, - serialized_end=21571, + serialized_start=21073, + serialized_end=21590, ) _GETPROTOCOLVERSIONUPGRADESTATERESPONSE = _descriptor.Descriptor( @@ -6248,8 +6253,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=20889, - serialized_end=21582, + serialized_start=20908, + serialized_end=21601, ) @@ -6294,8 +6299,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=21762, - serialized_end=21865, + serialized_start=21781, + serialized_end=21884, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSREQUEST = _descriptor.Descriptor( @@ -6330,8 +6335,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=21585, - serialized_end=21876, + serialized_start=21604, + serialized_end=21895, ) @@ -6362,8 +6367,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=22379, - serialized_end=22554, + serialized_start=22398, + serialized_end=22573, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSE_GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSEV0_VERSIONSIGNAL = _descriptor.Descriptor( @@ -6400,8 +6405,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=22556, - serialized_end=22609, + serialized_start=22575, + serialized_end=22628, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSE_GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSEV0 = _descriptor.Descriptor( @@ -6450,8 +6455,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22060, - serialized_end=22619, + serialized_start=22079, + serialized_end=22638, ) _GETPROTOCOLVERSIONUPGRADEVOTESTATUSRESPONSE = _descriptor.Descriptor( @@ -6486,8 +6491,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=21879, - serialized_end=22630, + serialized_start=21898, + serialized_end=22649, ) @@ -6539,8 +6544,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=22743, - serialized_end=22867, + serialized_start=22762, + serialized_end=22886, ) _GETEPOCHSINFOREQUEST = _descriptor.Descriptor( @@ -6575,8 +6580,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22633, - serialized_end=22878, + serialized_start=22652, + serialized_end=22897, ) @@ -6607,8 +6612,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=23239, - serialized_end=23356, + serialized_start=23258, + serialized_end=23375, ) _GETEPOCHSINFORESPONSE_GETEPOCHSINFORESPONSEV0_EPOCHINFO = _descriptor.Descriptor( @@ -6673,8 +6678,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=23359, - serialized_end=23525, + serialized_start=23378, + serialized_end=23544, ) _GETEPOCHSINFORESPONSE_GETEPOCHSINFORESPONSEV0 = _descriptor.Descriptor( @@ -6723,8 +6728,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22995, - serialized_end=23535, + serialized_start=23014, + serialized_end=23554, ) _GETEPOCHSINFORESPONSE = _descriptor.Descriptor( @@ -6759,8 +6764,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=22881, - serialized_end=23546, + serialized_start=22900, + serialized_end=23565, ) @@ -6819,8 +6824,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=23687, - serialized_end=23857, + serialized_start=23706, + serialized_end=23876, ) _GETFINALIZEDEPOCHINFOSREQUEST = _descriptor.Descriptor( @@ -6855,8 +6860,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=23549, - serialized_end=23868, + serialized_start=23568, + serialized_end=23887, ) @@ -6887,8 +6892,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=24294, - serialized_end=24458, + serialized_start=24313, + serialized_end=24477, ) _GETFINALIZEDEPOCHINFOSRESPONSE_GETFINALIZEDEPOCHINFOSRESPONSEV0_FINALIZEDEPOCHINFO = _descriptor.Descriptor( @@ -7002,8 +7007,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=24461, - serialized_end=25004, + serialized_start=24480, + serialized_end=25023, ) _GETFINALIZEDEPOCHINFOSRESPONSE_GETFINALIZEDEPOCHINFOSRESPONSEV0_BLOCKPROPOSER = _descriptor.Descriptor( @@ -7040,8 +7045,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=25006, - serialized_end=25063, + serialized_start=25025, + serialized_end=25082, ) _GETFINALIZEDEPOCHINFOSRESPONSE_GETFINALIZEDEPOCHINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -7090,8 +7095,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=24012, - serialized_end=25073, + serialized_start=24031, + serialized_end=25092, ) _GETFINALIZEDEPOCHINFOSRESPONSE = _descriptor.Descriptor( @@ -7126,8 +7131,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=23871, - serialized_end=25084, + serialized_start=23890, + serialized_end=25103, ) @@ -7165,8 +7170,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=25579, - serialized_end=25648, + serialized_start=25598, + serialized_end=25667, ) _GETCONTESTEDRESOURCESREQUEST_GETCONTESTEDRESOURCESREQUESTV0 = _descriptor.Descriptor( @@ -7262,8 +7267,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25222, - serialized_end=25682, + serialized_start=25241, + serialized_end=25701, ) _GETCONTESTEDRESOURCESREQUEST = _descriptor.Descriptor( @@ -7298,8 +7303,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25087, - serialized_end=25693, + serialized_start=25106, + serialized_end=25712, ) @@ -7330,8 +7335,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=26135, - serialized_end=26195, + serialized_start=26154, + serialized_end=26214, ) _GETCONTESTEDRESOURCESRESPONSE_GETCONTESTEDRESOURCESRESPONSEV0 = _descriptor.Descriptor( @@ -7380,8 +7385,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25834, - serialized_end=26205, + serialized_start=25853, + serialized_end=26224, ) _GETCONTESTEDRESOURCESRESPONSE = _descriptor.Descriptor( @@ -7416,8 +7421,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=25696, - serialized_end=26216, + serialized_start=25715, + serialized_end=26235, ) @@ -7455,8 +7460,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=26729, - serialized_end=26802, + serialized_start=26748, + serialized_end=26821, ) _GETVOTEPOLLSBYENDDATEREQUEST_GETVOTEPOLLSBYENDDATEREQUESTV0_ENDATTIMEINFO = _descriptor.Descriptor( @@ -7493,8 +7498,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=26804, - serialized_end=26871, + serialized_start=26823, + serialized_end=26890, ) _GETVOTEPOLLSBYENDDATEREQUEST_GETVOTEPOLLSBYENDDATEREQUESTV0 = _descriptor.Descriptor( @@ -7579,8 +7584,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26354, - serialized_end=26930, + serialized_start=26373, + serialized_end=26949, ) _GETVOTEPOLLSBYENDDATEREQUEST = _descriptor.Descriptor( @@ -7615,8 +7620,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26219, - serialized_end=26941, + serialized_start=26238, + serialized_end=26960, ) @@ -7654,8 +7659,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=27390, - serialized_end=27476, + serialized_start=27409, + serialized_end=27495, ) _GETVOTEPOLLSBYENDDATERESPONSE_GETVOTEPOLLSBYENDDATERESPONSEV0_SERIALIZEDVOTEPOLLSBYTIMESTAMPS = _descriptor.Descriptor( @@ -7692,8 +7697,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=27479, - serialized_end=27694, + serialized_start=27498, + serialized_end=27713, ) _GETVOTEPOLLSBYENDDATERESPONSE_GETVOTEPOLLSBYENDDATERESPONSEV0 = _descriptor.Descriptor( @@ -7742,8 +7747,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=27082, - serialized_end=27704, + serialized_start=27101, + serialized_end=27723, ) _GETVOTEPOLLSBYENDDATERESPONSE = _descriptor.Descriptor( @@ -7778,8 +7783,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=26944, - serialized_end=27715, + serialized_start=26963, + serialized_end=27734, ) @@ -7817,8 +7822,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=28404, - serialized_end=28488, + serialized_start=28423, + serialized_end=28507, ) _GETCONTESTEDRESOURCEVOTESTATEREQUEST_GETCONTESTEDRESOURCEVOTESTATEREQUESTV0 = _descriptor.Descriptor( @@ -7915,8 +7920,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=27877, - serialized_end=28602, + serialized_start=27896, + serialized_end=28621, ) _GETCONTESTEDRESOURCEVOTESTATEREQUEST = _descriptor.Descriptor( @@ -7951,8 +7956,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=27718, - serialized_end=28613, + serialized_start=27737, + serialized_end=28632, ) @@ -8024,8 +8029,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=29113, - serialized_end=29587, + serialized_start=29132, + serialized_end=29606, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0_CONTESTEDRESOURCECONTENDERS = _descriptor.Descriptor( @@ -8091,8 +8096,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=29590, - serialized_end=30042, + serialized_start=29609, + serialized_end=30061, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0_CONTENDER = _descriptor.Descriptor( @@ -8146,8 +8151,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30044, - serialized_end=30151, + serialized_start=30063, + serialized_end=30170, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE_GETCONTESTEDRESOURCEVOTESTATERESPONSEV0 = _descriptor.Descriptor( @@ -8196,8 +8201,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=28778, - serialized_end=30161, + serialized_start=28797, + serialized_end=30180, ) _GETCONTESTEDRESOURCEVOTESTATERESPONSE = _descriptor.Descriptor( @@ -8232,8 +8237,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=28616, - serialized_end=30172, + serialized_start=28635, + serialized_end=30191, ) @@ -8271,8 +8276,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=28404, - serialized_end=28488, + serialized_start=28423, + serialized_end=28507, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYREQUEST_GETCONTESTEDRESOURCEVOTERSFORIDENTITYREQUESTV0 = _descriptor.Descriptor( @@ -8368,8 +8373,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30359, - serialized_end=30889, + serialized_start=30378, + serialized_end=30908, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYREQUEST = _descriptor.Descriptor( @@ -8404,8 +8409,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30175, - serialized_end=30900, + serialized_start=30194, + serialized_end=30919, ) @@ -8443,8 +8448,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=31440, - serialized_end=31507, + serialized_start=31459, + serialized_end=31526, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYRESPONSE_GETCONTESTEDRESOURCEVOTERSFORIDENTITYRESPONSEV0 = _descriptor.Descriptor( @@ -8493,8 +8498,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=31090, - serialized_end=31517, + serialized_start=31109, + serialized_end=31536, ) _GETCONTESTEDRESOURCEVOTERSFORIDENTITYRESPONSE = _descriptor.Descriptor( @@ -8529,8 +8534,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=30903, - serialized_end=31528, + serialized_start=30922, + serialized_end=31547, ) @@ -8568,8 +8573,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=32077, - serialized_end=32174, + serialized_start=32096, + serialized_end=32193, ) _GETCONTESTEDRESOURCEIDENTITYVOTESREQUEST_GETCONTESTEDRESOURCEIDENTITYVOTESREQUESTV0 = _descriptor.Descriptor( @@ -8639,8 +8644,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=31702, - serialized_end=32205, + serialized_start=31721, + serialized_end=32224, ) _GETCONTESTEDRESOURCEIDENTITYVOTESREQUEST = _descriptor.Descriptor( @@ -8675,8 +8680,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=31531, - serialized_end=32216, + serialized_start=31550, + serialized_end=32235, ) @@ -8714,8 +8719,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=32719, - serialized_end=32966, + serialized_start=32738, + serialized_end=32985, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0_RESOURCEVOTECHOICE = _descriptor.Descriptor( @@ -8758,8 +8763,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=32969, - serialized_end=33270, + serialized_start=32988, + serialized_end=33289, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0_CONTESTEDRESOURCEIDENTITYVOTE = _descriptor.Descriptor( @@ -8810,8 +8815,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=33273, - serialized_end=33550, + serialized_start=33292, + serialized_end=33569, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE_GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSEV0 = _descriptor.Descriptor( @@ -8860,8 +8865,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=32393, - serialized_end=33560, + serialized_start=32412, + serialized_end=33579, ) _GETCONTESTEDRESOURCEIDENTITYVOTESRESPONSE = _descriptor.Descriptor( @@ -8896,8 +8901,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=32219, - serialized_end=33571, + serialized_start=32238, + serialized_end=33590, ) @@ -8935,8 +8940,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=33735, - serialized_end=33803, + serialized_start=33754, + serialized_end=33822, ) _GETPREFUNDEDSPECIALIZEDBALANCEREQUEST = _descriptor.Descriptor( @@ -8971,8 +8976,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=33574, - serialized_end=33814, + serialized_start=33593, + serialized_end=33833, ) @@ -9022,8 +9027,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=33982, - serialized_end=34171, + serialized_start=34001, + serialized_end=34190, ) _GETPREFUNDEDSPECIALIZEDBALANCERESPONSE = _descriptor.Descriptor( @@ -9058,8 +9063,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=33817, - serialized_end=34182, + serialized_start=33836, + serialized_end=34201, ) @@ -9090,8 +9095,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=34331, - serialized_end=34382, + serialized_start=34350, + serialized_end=34401, ) _GETTOTALCREDITSINPLATFORMREQUEST = _descriptor.Descriptor( @@ -9126,8 +9131,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34185, - serialized_end=34393, + serialized_start=34204, + serialized_end=34412, ) @@ -9177,8 +9182,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34546, - serialized_end=34730, + serialized_start=34565, + serialized_end=34749, ) _GETTOTALCREDITSINPLATFORMRESPONSE = _descriptor.Descriptor( @@ -9213,8 +9218,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34396, - serialized_end=34741, + serialized_start=34415, + serialized_end=34760, ) @@ -9259,8 +9264,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=34860, - serialized_end=34929, + serialized_start=34879, + serialized_end=34948, ) _GETPATHELEMENTSREQUEST = _descriptor.Descriptor( @@ -9295,8 +9300,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34744, - serialized_end=34940, + serialized_start=34763, + serialized_end=34959, ) @@ -9327,8 +9332,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=35313, - serialized_end=35341, + serialized_start=35332, + serialized_end=35360, ) _GETPATHELEMENTSRESPONSE_GETPATHELEMENTSRESPONSEV0 = _descriptor.Descriptor( @@ -9377,8 +9382,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=35063, - serialized_end=35351, + serialized_start=35082, + serialized_end=35370, ) _GETPATHELEMENTSRESPONSE = _descriptor.Descriptor( @@ -9413,8 +9418,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=34943, - serialized_end=35362, + serialized_start=34962, + serialized_end=35381, ) @@ -9438,8 +9443,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=35463, - serialized_end=35483, + serialized_start=35482, + serialized_end=35502, ) _GETSTATUSREQUEST = _descriptor.Descriptor( @@ -9474,8 +9479,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=35365, - serialized_end=35494, + serialized_start=35384, + serialized_end=35513, ) @@ -9530,8 +9535,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36371, - serialized_end=36465, + serialized_start=36390, + serialized_end=36484, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION_PROTOCOL_TENDERDASH = _descriptor.Descriptor( @@ -9568,8 +9573,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36698, - serialized_end=36738, + serialized_start=36717, + serialized_end=36757, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION_PROTOCOL_DRIVE = _descriptor.Descriptor( @@ -9613,8 +9618,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36740, - serialized_end=36800, + serialized_start=36759, + serialized_end=36819, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION_PROTOCOL = _descriptor.Descriptor( @@ -9651,8 +9656,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36468, - serialized_end=36800, + serialized_start=36487, + serialized_end=36819, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_VERSION = _descriptor.Descriptor( @@ -9689,8 +9694,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=36158, - serialized_end=36800, + serialized_start=36177, + serialized_end=36819, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_TIME = _descriptor.Descriptor( @@ -9756,8 +9761,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36802, - serialized_end=36929, + serialized_start=36821, + serialized_end=36948, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_NODE = _descriptor.Descriptor( @@ -9799,8 +9804,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36931, - serialized_end=36991, + serialized_start=36950, + serialized_end=37010, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_CHAIN = _descriptor.Descriptor( @@ -9891,8 +9896,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=36994, - serialized_end=37301, + serialized_start=37013, + serialized_end=37320, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_NETWORK = _descriptor.Descriptor( @@ -9936,8 +9941,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37303, - serialized_end=37370, + serialized_start=37322, + serialized_end=37389, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0_STATESYNC = _descriptor.Descriptor( @@ -10016,8 +10021,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37373, - serialized_end=37634, + serialized_start=37392, + serialized_end=37653, ) _GETSTATUSRESPONSE_GETSTATUSRESPONSEV0 = _descriptor.Descriptor( @@ -10082,8 +10087,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=35599, - serialized_end=37634, + serialized_start=35618, + serialized_end=37653, ) _GETSTATUSRESPONSE = _descriptor.Descriptor( @@ -10118,8 +10123,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=35497, - serialized_end=37645, + serialized_start=35516, + serialized_end=37664, ) @@ -10143,8 +10148,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37782, - serialized_end=37814, + serialized_start=37801, + serialized_end=37833, ) _GETCURRENTQUORUMSINFOREQUEST = _descriptor.Descriptor( @@ -10179,8 +10184,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=37648, - serialized_end=37825, + serialized_start=37667, + serialized_end=37844, ) @@ -10225,8 +10230,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=37965, - serialized_end=38035, + serialized_start=37984, + serialized_end=38054, ) _GETCURRENTQUORUMSINFORESPONSE_VALIDATORSETV0 = _descriptor.Descriptor( @@ -10277,8 +10282,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=38038, - serialized_end=38213, + serialized_start=38057, + serialized_end=38232, ) _GETCURRENTQUORUMSINFORESPONSE_GETCURRENTQUORUMSINFORESPONSEV0 = _descriptor.Descriptor( @@ -10336,8 +10341,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=38216, - serialized_end=38490, + serialized_start=38235, + serialized_end=38509, ) _GETCURRENTQUORUMSINFORESPONSE = _descriptor.Descriptor( @@ -10372,8 +10377,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=37828, - serialized_end=38501, + serialized_start=37847, + serialized_end=38520, ) @@ -10418,8 +10423,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=38647, - serialized_end=38737, + serialized_start=38666, + serialized_end=38756, ) _GETIDENTITYTOKENBALANCESREQUEST = _descriptor.Descriptor( @@ -10454,8 +10459,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=38504, - serialized_end=38748, + serialized_start=38523, + serialized_end=38767, ) @@ -10498,8 +10503,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39187, - serialized_end=39258, + serialized_start=39206, + serialized_end=39277, ) _GETIDENTITYTOKENBALANCESRESPONSE_GETIDENTITYTOKENBALANCESRESPONSEV0_TOKENBALANCES = _descriptor.Descriptor( @@ -10529,8 +10534,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=39261, - serialized_end=39415, + serialized_start=39280, + serialized_end=39434, ) _GETIDENTITYTOKENBALANCESRESPONSE_GETIDENTITYTOKENBALANCESRESPONSEV0 = _descriptor.Descriptor( @@ -10579,8 +10584,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=38898, - serialized_end=39425, + serialized_start=38917, + serialized_end=39444, ) _GETIDENTITYTOKENBALANCESRESPONSE = _descriptor.Descriptor( @@ -10615,8 +10620,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=38751, - serialized_end=39436, + serialized_start=38770, + serialized_end=39455, ) @@ -10661,8 +10666,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=39588, - serialized_end=39680, + serialized_start=39607, + serialized_end=39699, ) _GETIDENTITIESTOKENBALANCESREQUEST = _descriptor.Descriptor( @@ -10697,8 +10702,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39439, - serialized_end=39691, + serialized_start=39458, + serialized_end=39710, ) @@ -10741,8 +10746,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40159, - serialized_end=40241, + serialized_start=40178, + serialized_end=40260, ) _GETIDENTITIESTOKENBALANCESRESPONSE_GETIDENTITIESTOKENBALANCESRESPONSEV0_IDENTITYTOKENBALANCES = _descriptor.Descriptor( @@ -10772,8 +10777,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=40244, - serialized_end=40427, + serialized_start=40263, + serialized_end=40446, ) _GETIDENTITIESTOKENBALANCESRESPONSE_GETIDENTITIESTOKENBALANCESRESPONSEV0 = _descriptor.Descriptor( @@ -10822,8 +10827,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39847, - serialized_end=40437, + serialized_start=39866, + serialized_end=40456, ) _GETIDENTITIESTOKENBALANCESRESPONSE = _descriptor.Descriptor( @@ -10858,8 +10863,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=39694, - serialized_end=40448, + serialized_start=39713, + serialized_end=40467, ) @@ -10904,8 +10909,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=40585, - serialized_end=40672, + serialized_start=40604, + serialized_end=40691, ) _GETIDENTITYTOKENINFOSREQUEST = _descriptor.Descriptor( @@ -10940,8 +10945,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40451, - serialized_end=40683, + serialized_start=40470, + serialized_end=40702, ) @@ -10972,8 +10977,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=41097, - serialized_end=41137, + serialized_start=41116, + serialized_end=41156, ) _GETIDENTITYTOKENINFOSRESPONSE_GETIDENTITYTOKENINFOSRESPONSEV0_TOKENINFOENTRY = _descriptor.Descriptor( @@ -11015,8 +11020,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41140, - serialized_end=41316, + serialized_start=41159, + serialized_end=41335, ) _GETIDENTITYTOKENINFOSRESPONSE_GETIDENTITYTOKENINFOSRESPONSEV0_TOKENINFOS = _descriptor.Descriptor( @@ -11046,8 +11051,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=41319, - serialized_end=41457, + serialized_start=41338, + serialized_end=41476, ) _GETIDENTITYTOKENINFOSRESPONSE_GETIDENTITYTOKENINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -11096,8 +11101,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40824, - serialized_end=41467, + serialized_start=40843, + serialized_end=41486, ) _GETIDENTITYTOKENINFOSRESPONSE = _descriptor.Descriptor( @@ -11132,8 +11137,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=40686, - serialized_end=41478, + serialized_start=40705, + serialized_end=41497, ) @@ -11178,8 +11183,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=41621, - serialized_end=41710, + serialized_start=41640, + serialized_end=41729, ) _GETIDENTITIESTOKENINFOSREQUEST = _descriptor.Descriptor( @@ -11214,8 +11219,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41481, - serialized_end=41721, + serialized_start=41500, + serialized_end=41740, ) @@ -11246,8 +11251,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=41097, - serialized_end=41137, + serialized_start=41116, + serialized_end=41156, ) _GETIDENTITIESTOKENINFOSRESPONSE_GETIDENTITIESTOKENINFOSRESPONSEV0_TOKENINFOENTRY = _descriptor.Descriptor( @@ -11289,8 +11294,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42208, - serialized_end=42391, + serialized_start=42227, + serialized_end=42410, ) _GETIDENTITIESTOKENINFOSRESPONSE_GETIDENTITIESTOKENINFOSRESPONSEV0_IDENTITYTOKENINFOS = _descriptor.Descriptor( @@ -11320,8 +11325,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=42394, - serialized_end=42545, + serialized_start=42413, + serialized_end=42564, ) _GETIDENTITIESTOKENINFOSRESPONSE_GETIDENTITIESTOKENINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -11370,8 +11375,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41868, - serialized_end=42555, + serialized_start=41887, + serialized_end=42574, ) _GETIDENTITIESTOKENINFOSRESPONSE = _descriptor.Descriptor( @@ -11406,8 +11411,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=41724, - serialized_end=42566, + serialized_start=41743, + serialized_end=42585, ) @@ -11445,8 +11450,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=42688, - serialized_end=42749, + serialized_start=42707, + serialized_end=42768, ) _GETTOKENSTATUSESREQUEST = _descriptor.Descriptor( @@ -11481,8 +11486,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42569, - serialized_end=42760, + serialized_start=42588, + serialized_end=42779, ) @@ -11525,8 +11530,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43150, - serialized_end=43218, + serialized_start=43169, + serialized_end=43237, ) _GETTOKENSTATUSESRESPONSE_GETTOKENSTATUSESRESPONSEV0_TOKENSTATUSES = _descriptor.Descriptor( @@ -11556,8 +11561,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=43221, - serialized_end=43357, + serialized_start=43240, + serialized_end=43376, ) _GETTOKENSTATUSESRESPONSE_GETTOKENSTATUSESRESPONSEV0 = _descriptor.Descriptor( @@ -11606,8 +11611,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42886, - serialized_end=43367, + serialized_start=42905, + serialized_end=43386, ) _GETTOKENSTATUSESRESPONSE = _descriptor.Descriptor( @@ -11642,8 +11647,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=42763, - serialized_end=43378, + serialized_start=42782, + serialized_end=43397, ) @@ -11681,8 +11686,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=43536, - serialized_end=43609, + serialized_start=43555, + serialized_end=43628, ) _GETTOKENDIRECTPURCHASEPRICESREQUEST = _descriptor.Descriptor( @@ -11717,8 +11722,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43381, - serialized_end=43620, + serialized_start=43400, + serialized_end=43639, ) @@ -11756,8 +11761,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44110, - serialized_end=44161, + serialized_start=44129, + serialized_end=44180, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0_PRICINGSCHEDULE = _descriptor.Descriptor( @@ -11787,8 +11792,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44164, - serialized_end=44331, + serialized_start=44183, + serialized_end=44350, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0_TOKENDIRECTPURCHASEPRICEENTRY = _descriptor.Descriptor( @@ -11837,8 +11842,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=44334, - serialized_end=44562, + serialized_start=44353, + serialized_end=44581, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0_TOKENDIRECTPURCHASEPRICES = _descriptor.Descriptor( @@ -11868,8 +11873,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44565, - serialized_end=44765, + serialized_start=44584, + serialized_end=44784, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE_GETTOKENDIRECTPURCHASEPRICESRESPONSEV0 = _descriptor.Descriptor( @@ -11918,8 +11923,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43782, - serialized_end=44775, + serialized_start=43801, + serialized_end=44794, ) _GETTOKENDIRECTPURCHASEPRICESRESPONSE = _descriptor.Descriptor( @@ -11954,8 +11959,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=43623, - serialized_end=44786, + serialized_start=43642, + serialized_end=44805, ) @@ -11993,8 +11998,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44920, - serialized_end=44984, + serialized_start=44939, + serialized_end=45003, ) _GETTOKENCONTRACTINFOREQUEST = _descriptor.Descriptor( @@ -12029,8 +12034,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=44789, - serialized_end=44995, + serialized_start=44808, + serialized_end=45014, ) @@ -12068,8 +12073,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=45407, - serialized_end=45484, + serialized_start=45426, + serialized_end=45503, ) _GETTOKENCONTRACTINFORESPONSE_GETTOKENCONTRACTINFORESPONSEV0 = _descriptor.Descriptor( @@ -12118,8 +12123,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45133, - serialized_end=45494, + serialized_start=45152, + serialized_end=45513, ) _GETTOKENCONTRACTINFORESPONSE = _descriptor.Descriptor( @@ -12154,8 +12159,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=44998, - serialized_end=45505, + serialized_start=45017, + serialized_end=45524, ) @@ -12210,8 +12215,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45938, - serialized_end=46092, + serialized_start=45957, + serialized_end=46111, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSREQUEST_GETTOKENPREPROGRAMMEDDISTRIBUTIONSREQUESTV0 = _descriptor.Descriptor( @@ -12272,8 +12277,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45682, - serialized_end=46120, + serialized_start=45701, + serialized_end=46139, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSREQUEST = _descriptor.Descriptor( @@ -12308,8 +12313,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=45508, - serialized_end=46131, + serialized_start=45527, + serialized_end=46150, ) @@ -12347,8 +12352,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=46642, - serialized_end=46704, + serialized_start=46661, + serialized_end=46723, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE_GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSEV0_TOKENTIMEDDISTRIBUTIONENTRY = _descriptor.Descriptor( @@ -12385,8 +12390,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=46707, - serialized_end=46919, + serialized_start=46726, + serialized_end=46938, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE_GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSEV0_TOKENDISTRIBUTIONS = _descriptor.Descriptor( @@ -12416,8 +12421,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=46922, - serialized_end=47117, + serialized_start=46941, + serialized_end=47136, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE_GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSEV0 = _descriptor.Descriptor( @@ -12466,8 +12471,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=46312, - serialized_end=47127, + serialized_start=46331, + serialized_end=47146, ) _GETTOKENPREPROGRAMMEDDISTRIBUTIONSRESPONSE = _descriptor.Descriptor( @@ -12502,8 +12507,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=46134, - serialized_end=47138, + serialized_start=46153, + serialized_end=47157, ) @@ -12541,8 +12546,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=47327, - serialized_end=47400, + serialized_start=47346, + serialized_end=47419, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMREQUEST_GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMREQUESTV0 = _descriptor.Descriptor( @@ -12598,8 +12603,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47403, - serialized_end=47644, + serialized_start=47422, + serialized_end=47663, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMREQUEST = _descriptor.Descriptor( @@ -12634,8 +12639,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47141, - serialized_end=47655, + serialized_start=47160, + serialized_end=47674, ) @@ -12692,8 +12697,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48176, - serialized_end=48296, + serialized_start=48195, + serialized_end=48315, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMRESPONSE_GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMRESPONSEV0 = _descriptor.Descriptor( @@ -12742,8 +12747,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47848, - serialized_end=48306, + serialized_start=47867, + serialized_end=48325, ) _GETTOKENPERPETUALDISTRIBUTIONLASTCLAIMRESPONSE = _descriptor.Descriptor( @@ -12778,8 +12783,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=47658, - serialized_end=48317, + serialized_start=47677, + serialized_end=48336, ) @@ -12817,8 +12822,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=48448, - serialized_end=48511, + serialized_start=48467, + serialized_end=48530, ) _GETTOKENTOTALSUPPLYREQUEST = _descriptor.Descriptor( @@ -12853,8 +12858,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48320, - serialized_end=48522, + serialized_start=48339, + serialized_end=48541, ) @@ -12899,8 +12904,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=48943, - serialized_end=49063, + serialized_start=48962, + serialized_end=49082, ) _GETTOKENTOTALSUPPLYRESPONSE_GETTOKENTOTALSUPPLYRESPONSEV0 = _descriptor.Descriptor( @@ -12949,8 +12954,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48657, - serialized_end=49073, + serialized_start=48676, + serialized_end=49092, ) _GETTOKENTOTALSUPPLYRESPONSE = _descriptor.Descriptor( @@ -12985,8 +12990,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=48525, - serialized_end=49084, + serialized_start=48544, + serialized_end=49103, ) @@ -13031,8 +13036,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49194, - serialized_end=49286, + serialized_start=49213, + serialized_end=49305, ) _GETGROUPINFOREQUEST = _descriptor.Descriptor( @@ -13067,8 +13072,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49087, - serialized_end=49297, + serialized_start=49106, + serialized_end=49316, ) @@ -13106,8 +13111,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49655, - serialized_end=49707, + serialized_start=49674, + serialized_end=49726, ) _GETGROUPINFORESPONSE_GETGROUPINFORESPONSEV0_GROUPINFOENTRY = _descriptor.Descriptor( @@ -13144,8 +13149,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49710, - serialized_end=49862, + serialized_start=49729, + serialized_end=49881, ) _GETGROUPINFORESPONSE_GETGROUPINFORESPONSEV0_GROUPINFO = _descriptor.Descriptor( @@ -13180,8 +13185,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49865, - serialized_end=50003, + serialized_start=49884, + serialized_end=50022, ) _GETGROUPINFORESPONSE_GETGROUPINFORESPONSEV0 = _descriptor.Descriptor( @@ -13230,8 +13235,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49411, - serialized_end=50013, + serialized_start=49430, + serialized_end=50032, ) _GETGROUPINFORESPONSE = _descriptor.Descriptor( @@ -13266,8 +13271,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=49300, - serialized_end=50024, + serialized_start=49319, + serialized_end=50043, ) @@ -13305,8 +13310,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=50137, - serialized_end=50254, + serialized_start=50156, + serialized_end=50273, ) _GETGROUPINFOSREQUEST_GETGROUPINFOSREQUESTV0 = _descriptor.Descriptor( @@ -13367,8 +13372,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50257, - serialized_end=50509, + serialized_start=50276, + serialized_end=50528, ) _GETGROUPINFOSREQUEST = _descriptor.Descriptor( @@ -13403,8 +13408,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50027, - serialized_end=50520, + serialized_start=50046, + serialized_end=50539, ) @@ -13442,8 +13447,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=49655, - serialized_end=49707, + serialized_start=49674, + serialized_end=49726, ) _GETGROUPINFOSRESPONSE_GETGROUPINFOSRESPONSEV0_GROUPPOSITIONINFOENTRY = _descriptor.Descriptor( @@ -13487,8 +13492,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=50941, - serialized_end=51136, + serialized_start=50960, + serialized_end=51155, ) _GETGROUPINFOSRESPONSE_GETGROUPINFOSRESPONSEV0_GROUPINFOS = _descriptor.Descriptor( @@ -13518,8 +13523,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=51139, - serialized_end=51269, + serialized_start=51158, + serialized_end=51288, ) _GETGROUPINFOSRESPONSE_GETGROUPINFOSRESPONSEV0 = _descriptor.Descriptor( @@ -13568,8 +13573,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50637, - serialized_end=51279, + serialized_start=50656, + serialized_end=51298, ) _GETGROUPINFOSRESPONSE = _descriptor.Descriptor( @@ -13604,8 +13609,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=50523, - serialized_end=51290, + serialized_start=50542, + serialized_end=51309, ) @@ -13643,8 +13648,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=51409, - serialized_end=51485, + serialized_start=51428, + serialized_end=51504, ) _GETGROUPACTIONSREQUEST_GETGROUPACTIONSREQUESTV0 = _descriptor.Descriptor( @@ -13719,8 +13724,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51488, - serialized_end=51816, + serialized_start=51507, + serialized_end=51835, ) _GETGROUPACTIONSREQUEST = _descriptor.Descriptor( @@ -13756,8 +13761,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51293, - serialized_end=51867, + serialized_start=51312, + serialized_end=51886, ) @@ -13807,8 +13812,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52249, - serialized_end=52340, + serialized_start=52268, + serialized_end=52359, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNEVENT = _descriptor.Descriptor( @@ -13857,8 +13862,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52342, - serialized_end=52433, + serialized_start=52361, + serialized_end=52452, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_FREEZEEVENT = _descriptor.Descriptor( @@ -13900,8 +13905,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52435, - serialized_end=52509, + serialized_start=52454, + serialized_end=52528, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UNFREEZEEVENT = _descriptor.Descriptor( @@ -13943,8 +13948,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52511, - serialized_end=52587, + serialized_start=52530, + serialized_end=52606, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DESTROYFROZENFUNDSEVENT = _descriptor.Descriptor( @@ -13993,8 +13998,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52589, - serialized_end=52691, + serialized_start=52608, + serialized_end=52710, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_SHAREDENCRYPTEDNOTE = _descriptor.Descriptor( @@ -14038,8 +14043,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=52693, - serialized_end=52793, + serialized_start=52712, + serialized_end=52812, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_PERSONALENCRYPTEDNOTE = _descriptor.Descriptor( @@ -14083,8 +14088,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=52795, - serialized_end=52918, + serialized_start=52814, + serialized_end=52937, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT = _descriptor.Descriptor( @@ -14127,8 +14132,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=52921, - serialized_end=53154, + serialized_start=52940, + serialized_end=53173, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT = _descriptor.Descriptor( @@ -14170,8 +14175,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=53156, - serialized_end=53256, + serialized_start=53175, + serialized_end=53275, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT_PRICEFORQUANTITY = _descriptor.Descriptor( @@ -14208,8 +14213,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=44110, - serialized_end=44161, + serialized_start=44129, + serialized_end=44180, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT_PRICINGSCHEDULE = _descriptor.Descriptor( @@ -14239,8 +14244,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=53548, - serialized_end=53720, + serialized_start=53567, + serialized_end=53739, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT = _descriptor.Descriptor( @@ -14294,8 +14299,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=53259, - serialized_end=53745, + serialized_start=53278, + serialized_end=53764, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT = _descriptor.Descriptor( @@ -14344,8 +14349,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=53748, - serialized_end=54128, + serialized_start=53767, + serialized_end=54147, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT = _descriptor.Descriptor( @@ -14380,8 +14385,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=54131, - serialized_end=54270, + serialized_start=54150, + serialized_end=54289, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT = _descriptor.Descriptor( @@ -14411,8 +14416,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=54272, - serialized_end=54319, + serialized_start=54291, + serialized_end=54338, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT = _descriptor.Descriptor( @@ -14442,8 +14447,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=54321, - serialized_end=54368, + serialized_start=54340, + serialized_end=54387, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT = _descriptor.Descriptor( @@ -14478,8 +14483,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=54371, - serialized_end=54510, + serialized_start=54390, + serialized_end=54529, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT = _descriptor.Descriptor( @@ -14563,8 +14568,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=54513, - serialized_end=55490, + serialized_start=54532, + serialized_end=55509, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY = _descriptor.Descriptor( @@ -14601,8 +14606,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=55493, - serialized_end=55640, + serialized_start=55512, + serialized_end=55659, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS = _descriptor.Descriptor( @@ -14632,8 +14637,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=55643, - serialized_end=55775, + serialized_start=55662, + serialized_end=55794, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 = _descriptor.Descriptor( @@ -14682,8 +14687,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51990, - serialized_end=55785, + serialized_start=52009, + serialized_end=55804, ) _GETGROUPACTIONSRESPONSE = _descriptor.Descriptor( @@ -14718,8 +14723,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=51870, - serialized_end=55796, + serialized_start=51889, + serialized_end=55815, ) @@ -14778,8 +14783,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=55934, - serialized_end=56140, + serialized_start=55953, + serialized_end=56159, ) _GETGROUPACTIONSIGNERSREQUEST = _descriptor.Descriptor( @@ -14815,8 +14820,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=55799, - serialized_end=56191, + serialized_start=55818, + serialized_end=56210, ) @@ -14854,8 +14859,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=56623, - serialized_end=56676, + serialized_start=56642, + serialized_end=56695, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0_GROUPACTIONSIGNERS = _descriptor.Descriptor( @@ -14885,8 +14890,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=56679, - serialized_end=56824, + serialized_start=56698, + serialized_end=56843, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0 = _descriptor.Descriptor( @@ -14935,8 +14940,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56332, - serialized_end=56834, + serialized_start=56351, + serialized_end=56853, ) _GETGROUPACTIONSIGNERSRESPONSE = _descriptor.Descriptor( @@ -14971,8 +14976,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56194, - serialized_end=56845, + serialized_start=56213, + serialized_end=56864, ) @@ -15010,8 +15015,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=56961, - serialized_end=57018, + serialized_start=56980, + serialized_end=57037, ) _GETADDRESSINFOREQUEST = _descriptor.Descriptor( @@ -15046,8 +15051,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=56848, - serialized_end=57029, + serialized_start=56867, + serialized_end=57048, ) @@ -15090,8 +15095,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57032, - serialized_end=57165, + serialized_start=57051, + serialized_end=57184, ) @@ -15129,8 +15134,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57167, - serialized_end=57216, + serialized_start=57186, + serialized_end=57235, ) @@ -15161,8 +15166,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57218, - serialized_end=57313, + serialized_start=57237, + serialized_end=57332, ) @@ -15212,8 +15217,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57315, - serialized_end=57424, + serialized_start=57334, + serialized_end=57443, ) @@ -15251,8 +15256,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57426, - serialized_end=57546, + serialized_start=57445, + serialized_end=57565, ) @@ -15283,8 +15288,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57548, - serialized_end=57655, + serialized_start=57567, + serialized_end=57674, ) @@ -15334,8 +15339,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57775, - serialized_end=58000, + serialized_start=57794, + serialized_end=58019, ) _GETADDRESSINFORESPONSE = _descriptor.Descriptor( @@ -15370,8 +15375,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=57658, - serialized_end=58011, + serialized_start=57677, + serialized_end=58030, ) @@ -15409,8 +15414,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=58136, - serialized_end=58198, + serialized_start=58155, + serialized_end=58217, ) _GETADDRESSESINFOSREQUEST = _descriptor.Descriptor( @@ -15445,8 +15450,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58014, - serialized_end=58209, + serialized_start=58033, + serialized_end=58228, ) @@ -15496,8 +15501,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58338, - serialized_end=58570, + serialized_start=58357, + serialized_end=58589, ) _GETADDRESSESINFOSRESPONSE = _descriptor.Descriptor( @@ -15532,8 +15537,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58212, - serialized_end=58581, + serialized_start=58231, + serialized_end=58600, ) @@ -15557,8 +15562,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=58721, - serialized_end=58754, + serialized_start=58740, + serialized_end=58773, ) _GETADDRESSESTRUNKSTATEREQUEST = _descriptor.Descriptor( @@ -15593,8 +15598,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58584, - serialized_end=58765, + serialized_start=58603, + serialized_end=58784, ) @@ -15632,8 +15637,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=58909, - serialized_end=59055, + serialized_start=58928, + serialized_end=59074, ) _GETADDRESSESTRUNKSTATERESPONSE = _descriptor.Descriptor( @@ -15668,8 +15673,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=58768, - serialized_end=59066, + serialized_start=58787, + serialized_end=59085, ) @@ -15714,8 +15719,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=59209, - serialized_end=59298, + serialized_start=59228, + serialized_end=59317, ) _GETADDRESSESBRANCHSTATEREQUEST = _descriptor.Descriptor( @@ -15750,8 +15755,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59069, - serialized_end=59309, + serialized_start=59088, + serialized_end=59328, ) @@ -15782,8 +15787,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=59455, - serialized_end=59510, + serialized_start=59474, + serialized_end=59529, ) _GETADDRESSESBRANCHSTATERESPONSE = _descriptor.Descriptor( @@ -15818,8 +15823,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59312, - serialized_end=59521, + serialized_start=59331, + serialized_end=59540, ) @@ -15864,8 +15869,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=59685, - serialized_end=59799, + serialized_start=59704, + serialized_end=59818, ) _GETRECENTADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -15900,8 +15905,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59524, - serialized_end=59810, + serialized_start=59543, + serialized_end=59829, ) @@ -15951,8 +15956,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59978, - serialized_end=60242, + serialized_start=59997, + serialized_end=60261, ) _GETRECENTADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -15987,8 +15992,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=59813, - serialized_end=60253, + serialized_start=59832, + serialized_end=60272, ) @@ -16026,8 +16031,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60255, - serialized_end=60326, + serialized_start=60274, + serialized_end=60345, ) @@ -16077,8 +16082,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=60329, - serialized_end=60505, + serialized_start=60348, + serialized_end=60524, ) @@ -16109,8 +16114,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60507, - serialized_end=60599, + serialized_start=60526, + serialized_end=60618, ) @@ -16155,8 +16160,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60602, - serialized_end=60776, + serialized_start=60621, + serialized_end=60795, ) @@ -16187,8 +16192,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=60779, - serialized_end=60914, + serialized_start=60798, + serialized_end=60933, ) @@ -16226,8 +16231,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=61106, - serialized_end=61203, + serialized_start=61125, + serialized_end=61222, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -16262,8 +16267,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=60917, - serialized_end=61214, + serialized_start=60936, + serialized_end=61233, ) @@ -16313,8 +16318,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61410, - serialized_end=61702, + serialized_start=61429, + serialized_end=61721, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -16349,8 +16354,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61217, - serialized_end=61713, + serialized_start=61236, + serialized_end=61732, ) @@ -16395,8 +16400,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=61862, - serialized_end=61949, + serialized_start=61881, + serialized_end=61968, ) _GETSHIELDEDENCRYPTEDNOTESREQUEST = _descriptor.Descriptor( @@ -16431,8 +16436,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61716, - serialized_end=61960, + serialized_start=61735, + serialized_end=61979, ) @@ -16484,8 +16489,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=62407, - serialized_end=62494, + serialized_start=62426, + serialized_end=62513, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0_ENCRYPTEDNOTES = _descriptor.Descriptor( @@ -16515,8 +16520,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=62497, - serialized_end=62642, + serialized_start=62516, + serialized_end=62661, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0 = _descriptor.Descriptor( @@ -16565,8 +16570,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62113, - serialized_end=62652, + serialized_start=62132, + serialized_end=62671, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE = _descriptor.Descriptor( @@ -16601,8 +16606,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=61963, - serialized_end=62663, + serialized_start=61982, + serialized_end=62682, ) @@ -16633,8 +16638,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=62791, - serialized_end=62835, + serialized_start=62810, + serialized_end=62854, ) _GETSHIELDEDANCHORSREQUEST = _descriptor.Descriptor( @@ -16669,8 +16674,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62666, - serialized_end=62846, + serialized_start=62685, + serialized_end=62865, ) @@ -16701,8 +16706,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=63235, - serialized_end=63261, + serialized_start=63254, + serialized_end=63280, ) _GETSHIELDEDANCHORSRESPONSE_GETSHIELDEDANCHORSRESPONSEV0 = _descriptor.Descriptor( @@ -16751,8 +16756,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62978, - serialized_end=63271, + serialized_start=62997, + serialized_end=63290, ) _GETSHIELDEDANCHORSRESPONSE = _descriptor.Descriptor( @@ -16787,8 +16792,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=62849, - serialized_end=63282, + serialized_start=62868, + serialized_end=63301, ) @@ -16819,8 +16824,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=63437, - serialized_end=63490, + serialized_start=63456, + serialized_end=63509, ) _GETMOSTRECENTSHIELDEDANCHORREQUEST = _descriptor.Descriptor( @@ -16855,8 +16860,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63285, - serialized_end=63501, + serialized_start=63304, + serialized_end=63520, ) @@ -16906,8 +16911,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63660, - serialized_end=63841, + serialized_start=63679, + serialized_end=63860, ) _GETMOSTRECENTSHIELDEDANCHORRESPONSE = _descriptor.Descriptor( @@ -16942,8 +16947,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63504, - serialized_end=63852, + serialized_start=63523, + serialized_end=63871, ) @@ -16974,8 +16979,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=63986, - serialized_end=64032, + serialized_start=64005, + serialized_end=64051, ) _GETSHIELDEDPOOLSTATEREQUEST = _descriptor.Descriptor( @@ -17010,8 +17015,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=63855, - serialized_end=64043, + serialized_start=63874, + serialized_end=64062, ) @@ -17061,8 +17066,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64181, - serialized_end=64366, + serialized_start=64200, + serialized_end=64385, ) _GETSHIELDEDPOOLSTATERESPONSE = _descriptor.Descriptor( @@ -17097,8 +17102,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64046, - serialized_end=64377, + serialized_start=64065, + serialized_end=64396, ) @@ -17129,8 +17134,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=64514, - serialized_end=64561, + serialized_start=64533, + serialized_end=64580, ) _GETSHIELDEDNOTESCOUNTREQUEST = _descriptor.Descriptor( @@ -17165,8 +17170,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64380, - serialized_end=64572, + serialized_start=64399, + serialized_end=64591, ) @@ -17216,8 +17221,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64713, - serialized_end=64903, + serialized_start=64732, + serialized_end=64922, ) _GETSHIELDEDNOTESCOUNTRESPONSE = _descriptor.Descriptor( @@ -17252,8 +17257,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64575, - serialized_end=64914, + serialized_start=64594, + serialized_end=64933, ) @@ -17291,8 +17296,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=65051, - serialized_end=65118, + serialized_start=65070, + serialized_end=65137, ) _GETSHIELDEDNULLIFIERSREQUEST = _descriptor.Descriptor( @@ -17327,8 +17332,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=64917, - serialized_end=65129, + serialized_start=64936, + serialized_end=65148, ) @@ -17366,8 +17371,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=65558, - serialized_end=65612, + serialized_start=65577, + serialized_end=65631, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0_NULLIFIERSTATUSES = _descriptor.Descriptor( @@ -17397,8 +17402,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=65615, - serialized_end=65757, + serialized_start=65634, + serialized_end=65776, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0 = _descriptor.Descriptor( @@ -17447,8 +17452,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=65270, - serialized_end=65767, + serialized_start=65289, + serialized_end=65786, ) _GETSHIELDEDNULLIFIERSRESPONSE = _descriptor.Descriptor( @@ -17483,8 +17488,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=65132, - serialized_end=65778, + serialized_start=65151, + serialized_end=65797, ) _GETIDENTITYREQUEST_GETIDENTITYREQUESTV0.containing_type = _GETIDENTITYREQUEST @@ -22475,8 +22480,8 @@ index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=65873, - serialized_end=74655, + serialized_start=65892, + serialized_end=74674, 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 c394bd38fe9..f66a16306e0 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 @@ -2739,6 +2739,7 @@ export namespace GetDocumentsRequest { BETWEEN_EXCLUDE_RIGHT: 8; IN: 9; STARTS_WITH: 10; + IN_TIME_RANGE: 11; } export const WhereOperator: WhereOperatorMap; 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 10f2641e792..ad740f8e1c9 100644 --- a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js +++ b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js @@ -24389,7 +24389,8 @@ proto.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator = { BETWEEN_EXCLUDE_LEFT: 7, BETWEEN_EXCLUDE_RIGHT: 8, IN: 9, - STARTS_WITH: 10 + STARTS_WITH: 10, + IN_TIME_RANGE: 11 }; diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 0ad3a197fd0..adb8f565c9d 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -594,6 +594,18 @@ message GetDocumentsRequest { BETWEEN_EXCLUDE_RIGHT = 8; IN = 9; STARTS_WITH = 10; + // Time-range bucket selection (v1 only; the v0 CBOR surface is + // unaffected). `field` names a timestamp covered by a `timeRange` + // index. Operand: `text` selector `"newest"`/`"oldest"` when one grid + // buckets the field, or `list` `[selector, range, step(, phase)]` in + // the contract's declared seconds to name one of several grids (zero + // phase is spelled by omission — one wire spelling per grid). The + // server resolves it to a bucket-start equality from current block + // time; the verifier re-derives the same bucket from the quorum-signed + // metadata time — an ordinary index/count proof. See `timeRange` in + // the document meta-schema and + // `drive::query::resolve_time_range_bucket_clause`. + IN_TIME_RANGE = 11; } // Tagged scalar (or list) operand for a `WhereClause`. The diff --git a/packages/dash-platform-queries/src/documents/average_proof_helpers.rs b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs index 5ab4e9bf77a..d0e99c5acf2 100644 --- a/packages/dash-platform-queries/src/documents/average_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs @@ -19,6 +19,7 @@ //! [`DocumentAverage`]: drive_proof_verifier::DocumentAverage //! [`DocumentSplitAverages`]: drive_proof_verifier::DocumentSplitAverages +use crate::documents::document_query::normalize_time_range_clauses_with_metadata_time; use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; @@ -33,6 +34,7 @@ use drive::query::drive_document_sum_query::index_picker::{ }; use drive::query::drive_document_sum_query::mode_detection::detect_sum_mode_from_inputs; use drive::query::drive_document_sum_query::{DocumentSumMode, DriveDocumentSumQuery, SumMode}; +use drive::query::validate_and_canonicalize_where_clauses; use drive::query::{SelectFunction, WhereOperator}; use drive_proof_verifier::{ verify_aggregate_count_and_sum_proof, verify_carrier_aggregate_count_and_sum_proof, @@ -96,11 +98,43 @@ pub(super) fn assert_select_is_avg( /// that should be unreachable here (`prove = true`); reject as /// `RequestError` if they bubble through. pub(super) fn verify_average_query( - request: DocumentQuery, + mut request: DocumentQuery, response: GetDocumentsResponse, platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let proof = response + .proof() + .or(Err(drive_proof_verifier::Error::NoProofInResult))?; + let mtd = response + .metadata() + .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?; + let contract_id = request.data_contract.id().to_buffer(); + let sum_property = request.select.field.clone(); + + // Resolve any pending time-range (`IN_TIME_RANGE`) selections into + // concrete bucket-equality clauses using the quorum-signed metadata + // block time — BEFORE mode detection and covering-index selection + // below, which read `request.where_clauses`; the prover routed on + // the resolved shape. + // ...and enforce the same provenance-vs-shape contract the server + // dispatchers do, through the one shared normalization helper. + let resolved_time_ranges = + normalize_time_range_clauses_with_metadata_time(&mut request, mtd.time_ms)?; + + // Canonicalize exactly as the server dispatcher does (the shared step + // in `drive::query::canonicalize`): the prover merged `[f > A, f < B]` + // pairs into one `between*` clause before its mode detection, so mode + // detection here must run over the same canonical shape or a valid + // proof is rejected. + request.where_clauses = validate_and_canonicalize_where_clauses( + std::mem::take(&mut request.where_clauses), + platform_version, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("where-clause canonicalization failed: {e}"), + })?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -110,14 +144,6 @@ pub(super) fn verify_average_query( 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 contract_id = request.data_contract.id().to_buffer(); - let sum_property = request.select.field.clone(); // Resolve the SQL-shape `SumMode` the request implies — AVG // shares the routing table with SUM (see module docstring), so @@ -174,6 +200,7 @@ pub(super) fn verify_average_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_ranges, ) .filter(|idx| idx.range_countable) .ok_or_else(|| drive_proof_verifier::Error::RequestError { @@ -189,6 +216,7 @@ pub(super) fn verify_average_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_ranges, ) .filter(|idx| idx.countable.is_countable()) .ok_or_else(|| drive_proof_verifier::Error::RequestError { diff --git a/packages/dash-platform-queries/src/documents/count_proof_helpers.rs b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs index e19e9074f53..cf060ecff31 100644 --- a/packages/dash-platform-queries/src/documents/count_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs @@ -13,6 +13,7 @@ //! [`DocumentCount`]: drive_proof_verifier::DocumentCount //! [`DocumentSplitCounts`]: drive_proof_verifier::DocumentSplitCounts +use crate::documents::document_query::normalize_time_range_clauses_with_metadata_time; use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; @@ -22,6 +23,7 @@ use dpp::{ data_contract::accessors::v0::DataContractV0Getters, data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}, }; +use drive::query::validate_and_canonicalize_where_clauses; use drive::query::{ CountMode, DocumentCountMode, DriveDocumentCountQuery, SelectFunction, WhereOperator, }; @@ -138,11 +140,41 @@ fn limit_to_u16_or_default(limit: u32) -> Result Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let proof = response + .proof() + .or(Err(drive_proof_verifier::Error::NoProofInResult))?; + let mtd = response + .metadata() + .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?; + + // Resolve any pending time-range (`IN_TIME_RANGE`) selections into + // concrete bucket-equality clauses using the quorum-signed metadata + // block time — BEFORE mode detection and covering-index selection + // below, which read `request.where_clauses`; the prover routed on + // the resolved shape. + // ...and enforce the same provenance-vs-shape contract the server + // dispatchers do, through the one shared normalization helper. + let resolved_time_ranges = + normalize_time_range_clauses_with_metadata_time(&mut request, mtd.time_ms)?; + + // Canonicalize exactly as the server dispatcher does (the shared step + // in `drive::query::canonicalize`): the prover merged `[f > A, f < B]` + // pairs into one `between*` clause before its mode detection, so mode + // detection here must run over the same canonical shape or a valid + // proof is rejected. + request.where_clauses = validate_and_canonicalize_where_clauses( + std::mem::take(&mut request.where_clauses), + platform_version, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("where-clause canonicalization failed: {e}"), + })?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -152,12 +184,6 @@ pub(super) fn verify_count_query( 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))?; // Resolve the SQL-shape `CountMode` the request implies. Same // decision tree as `validate_and_route` in the abci handler — @@ -224,6 +250,7 @@ pub(super) fn verify_count_query( DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &request.where_clauses, + &resolved_time_ranges, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "range count requires a `range_countable: true` index whose last \ @@ -234,6 +261,7 @@ pub(super) fn verify_count_query( DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &request.where_clauses, + &resolved_time_ranges, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "prove count requires a `countable: true` index whose properties \ @@ -406,3 +434,301 @@ fn single_empty_key_entry(count: u64) -> Vec { count: Some(count), }] } + +#[cfg(test)] +mod tests { + //! Offline tests for the client half of a time-range + //! (`IN_TIME_RANGE`) query: where the bucket comes from, what + //! provenance the resolution hands back, and how the count + //! surface behaves when a response cannot be verified. Nothing + //! here touches a proof — this crate builds drive with `verify` + //! only, so no Drive exists to prove against. The prove→verify + //! round trips (overlapping-window counts, a tampered signed + //! time, the documents route) live in rs-drive-abci's + //! `time_range_proof_verification`, where a populated platform + //! does. + //! + //! The property under test throughout is that the bucket is a + //! function of the **quorum-signed** metadata time and of the + //! contract's declared window, and of nothing else the client + //! could pick: a client-local clock would let a node answer with + //! whatever bucket it liked and still verify. + + use super::*; + use crate::documents::document_query::resolve_time_range_clauses_with_metadata_time; + use dapi_grpc::platform::v0::get_documents_response::get_documents_response_v1::{ + count_results, result_data, CountResults, ResultData, + }; + use dapi_grpc::platform::v0::get_documents_response::{ + get_documents_response_v1, GetDocumentsResponseV1, Version as ResponseVersion, + }; + use dash_context_provider::ContextProviderError; + use dpp::data_contract::{DataContractFactory, TokenConfiguration}; + use dpp::platform_value::platform_value; + use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; + use drive::query::{SelectProjection, TimeRangeSelector, WhereClause}; + use std::sync::Arc; + + const DOCUMENT_TYPE: &str = "post"; + const CREATED_AT: &str = "$createdAt"; + const BUCKETED_INDEX: &str = "trending"; + /// Six-hour windows sliding every two hours — overlap factor 3, the + /// shape a trending leaderboard actually declares. + const RANGE_SECONDS: u64 = 6 * 3_600; + const STEP_SECONDS: u64 = 2 * 3_600; + const STEP_MS: u64 = STEP_SECONDS * 1_000; + /// An exact multiple of the two-hour step, so on the `phase: 0` grid it + /// is itself a bucket start. + const BUCKET_START_MS: u64 = 1_755_000_000_000; + /// A one-hour phase — strictly less than the step, as validation + /// requires — for the epoch-sliver refusal test. + const PHASE_SECONDS: u64 = 3_600; + const PHASE_MS: u64 = PHASE_SECONDS * 1_000; + + fn platform_version() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + /// Never consulted by these tests — every one of them fails (or is + /// asserted) before a proof reaches the tenderdash binding. It exists + /// because [`verify_count_query`] takes a provider by reference. + struct UnusedProvider; + + impl ContextProvider for UnusedProvider { + fn get_data_contract( + &self, + _id: &Identifier, + _platform_version: &PlatformVersion, + ) -> Result>, ContextProviderError> { + Ok(None) + } + + fn get_token_configuration( + &self, + _token_id: &Identifier, + ) -> Result, ContextProviderError> { + Ok(None) + } + + fn get_quorum_public_key( + &self, + _quorum_type: u32, + _quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], ContextProviderError> { + Ok([0u8; 48]) + } + + fn get_platform_activation_height(&self) -> Result { + Ok(1) + } + } + + /// A contract whose `post` doctype carries a `countable` bucketed index + /// over `(timeRange($createdAt), hashtag)`. `phase_seconds` is a + /// parameter because the epoch-sliver refusal is one of the behaviours + /// under test. + fn trending_contract(phase_seconds: u64) -> Arc { + let schemas = platform_value!({ + "post": { + "type": "object", + "properties": { + "hashtag": { "type": "string", "maxLength": 63, "position": 0 }, + }, + "indices": [ + { + "name": "trending", + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "countable": true, + "timeRange": { + "on": "$createdAt", + "range": RANGE_SECONDS, + "step": STEP_SECONDS, + "phase": phase_seconds, + }, + }, + ], + "required": ["$createdAt", "hashtag"], + "additionalProperties": false, + } + }); + let contract = DataContractFactory::new(platform_version().protocol_version) + .expect("expected a factory") + .create_with_value_config(Identifier::new([7u8; 32]), 0, schemas, None, None) + .expect("the trending contract is well-formed") + .data_contract_owned(); + Arc::new(contract) + } + + /// The bucket start the contract's own transform puts `time_ms` in — + /// the expectation is derived from the declared window rather than + /// restated as a literal, so a fixture edit cannot leave it behind. + fn expected_bucket(contract: &DataContract, time_ms: u64) -> u64 { + contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists") + .indexes() + .get(BUCKETED_INDEX) + .expect("the bucketed index survives contract creation") + .time_range + .as_ref() + .expect("the bucketed index carries its transform") + .newest_active_start(time_ms) + .expect("the metadata time is inside an active range") + } + + fn newest_bucket_query(contract: Arc) -> DocumentQuery { + DocumentQuery::new(contract, DOCUMENT_TYPE) + .expect("the fixture has this document type") + .with_time_range(CREATED_AT, TimeRangeSelector::Newest) + } + + fn resolved_equality(request: &DocumentQuery) -> &WhereClause { + let matching: Vec<_> = request + .where_clauses + .iter() + .filter(|clause| clause.field == CREATED_AT) + .collect(); + assert_eq!( + matching.len(), + 1, + "resolution must push exactly one clause on the bucketed field" + ); + matching[0] + } + + /// The whole contract of the resolution step: the pending selector is + /// consumed, an ordinary equality on the bucketed field appears in its + /// place, and the field name comes back as provenance — which is the + /// only thing that will later keep index selection on the bucketed + /// index, since the pushed clause is indistinguishable from a + /// hand-written raw-timestamp lookup. + #[test] + fn the_newest_selector_resolves_to_the_bucket_containing_the_metadata_time() { + let contract = trending_contract(0); + let mut request = newest_bucket_query(Arc::clone(&contract)); + let metadata_time_ms = BUCKET_START_MS + 3_600_000; + + let resolutions = + resolve_time_range_clauses_with_metadata_time(&mut request, metadata_time_ms) + .expect("a metadata time inside an active range resolves"); + + assert_eq!(resolutions.len(), 1); + assert_eq!(resolutions[0].field(), CREATED_AT); + assert_eq!( + resolutions[0].transform.range_seconds, RANGE_SECONDS, + "the provenance must carry the exact grid the resolution used" + ); + assert_eq!(resolutions[0].transform.step_seconds, STEP_SECONDS); + assert!( + request.time_range_clauses.is_empty(), + "the pending selector must be drained, not left to be encoded twice" + ); + let clause = resolved_equality(&request); + assert_eq!(clause.operator, WhereOperator::Equal); + assert_eq!( + clause.value, + dpp::platform_value::Value::U64(expected_bucket(&contract, metadata_time_ms)) + ); + assert_eq!( + clause.value, + dpp::platform_value::Value::U64(BUCKET_START_MS), + "one hour past a bucket start is still inside that bucket" + ); + } + + /// The same query against a metadata time one full step later resolves + /// one bucket later — pinning that the bucket is derived from the signed + /// time rather than from anything the client holds. If the resolution + /// ever started reading a local clock this assertion is what breaks. + #[test] + fn a_metadata_time_one_step_later_resolves_to_the_next_bucket() { + let contract = trending_contract(0); + let earlier_time_ms = BUCKET_START_MS + 3_600_000; + + let mut earlier = newest_bucket_query(Arc::clone(&contract)); + resolve_time_range_clauses_with_metadata_time(&mut earlier, earlier_time_ms) + .expect("a metadata time inside an active range resolves"); + + let mut later = newest_bucket_query(Arc::clone(&contract)); + resolve_time_range_clauses_with_metadata_time(&mut later, earlier_time_ms + STEP_MS) + .expect("a metadata time inside an active range resolves"); + + let earlier_bucket = resolved_equality(&earlier) + .value + .to_integer::() + .expect("a bucket start is a millisecond timestamp"); + let later_bucket = resolved_equality(&later) + .value + .to_integer::() + .expect("a bucket start is a millisecond timestamp"); + assert_eq!( + later_bucket, + earlier_bucket + STEP_MS, + "one step of signed time must move the resolution exactly one bucket" + ); + } + + /// A metadata time inside the epoch sliver before the grid's phase + /// anchor belongs to no range at all. No real block time reaches it, but + /// the client must refuse rather than invent a bucket, mirroring the + /// server, which refuses the same request at resolution time — so the + /// two sides cannot disagree about whether the query was answerable. + #[test] + fn a_metadata_time_in_the_epoch_sliver_refuses_to_resolve() { + let contract = trending_contract(PHASE_SECONDS); + let mut request = newest_bucket_query(contract); + + let error = resolve_time_range_clauses_with_metadata_time(&mut request, PHASE_MS - 1) + .expect_err("a time predating every range has no honest bucket"); + + match error { + drive_proof_verifier::Error::RequestError { error } => assert!( + error.contains("phase"), + "expected the epoch-sliver refusal, got: {error}" + ), + other => panic!("expected a request error, got: {other:?}"), + } + } + + /// A node that answers a `prove = true` time-range count with data + /// instead of a proof must be rejected as an unproven response, not + /// mistaken for a query the client failed to reconstruct: the proof + /// check is the first gate in [`verify_count_query`], ahead of the + /// time-range resolution and the provenance-shape guard. Callers + /// therefore get `NoProofInResult` — "this node did not prove it" — + /// rather than a resolution error that would send them looking at their + /// own query. + #[test] + fn a_count_response_carrying_no_proof_is_rejected_as_unproven() { + let contract = trending_contract(0); + let request = newest_bucket_query(contract) + .with_select(SelectProjection::count_star()) + .with_where(WhereClause { + field: "hashtag".to_string(), + operator: WhereOperator::Equal, + value: dpp::platform_value::Value::Text("ibiza".to_string()), + }); + + let response = GetDocumentsResponse { + version: Some(ResponseVersion::V1(GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Data(ResultData { + variant: Some(result_data::Variant::Counts(CountResults { + variant: Some(count_results::Variant::AggregateCount(2)), + })), + })), + metadata: Some(ResponseMetadata { + time_ms: BUCKET_START_MS + 3_600_000, + ..Default::default() + }), + })), + }; + + let error = verify_count_query(request, response, platform_version(), &UnusedProvider) + .expect_err("an unproven response must not be accepted"); + assert!( + matches!(error, drive_proof_verifier::Error::NoProofInResult), + "expected NoProofInResult, got: {error:?}" + ); + } +} diff --git a/packages/dash-platform-queries/src/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs index d7c7bf95908..cd710ddf40e 100644 --- a/packages/dash-platform-queries/src/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -33,14 +33,33 @@ use dpp::{ use drive::config::DEFAULT_QUERY_LIMIT; use drive::query::drive_document_ranked_query::mode_detection::ranked_order_key; use drive::query::{ + resolve_time_range_bucket_clause, validate_resolved_time_range_clause_shapes, DriveDocumentQuery, HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, - HavingRightOperand, InternalClauses, OrderClause, SelectFunction, SelectProjection, - WhereClause, WhereOperator, + HavingRightOperand, InternalClauses, OrderClause, ResolvedTimeRange, SelectFunction, + SelectProjection, TimeRangeGridSpec, TimeRangeSelector, WhereClause, WhereOperator, }; use drive_proof_verifier::{types::Documents, FromProof}; // TODO: remove DocumentQuery once ContextProvider that provides data contracts is merged. +/// One pending `IN_TIME_RANGE` selection: the timestamp field, the +/// `"newest"` / `"oldest"` selector, and — when the contract buckets the +/// field with more than one `timeRange` grid — the grid it targets (`None` +/// means the field's sole grid, and the server rejects the bare form on a +/// multi-grid field as ambiguous). +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))] +pub struct TimeRangeClause { + /// The bucketed timestamp field the selection is on. + pub field: String, + /// Which active window to resolve to. + pub selector: TimeRangeSelector, + /// The grid targeted, in the contract's declared seconds; `None` when + /// the field carries a single grid. + #[cfg_attr(feature = "mocks", serde(default))] + pub grid: Option, +} + /// Request that is used to query documents from the Dash Platform. /// /// This is an abstraction layer built on top of [GetDocumentsRequest] to address issues with missing details @@ -74,6 +93,15 @@ pub struct DocumentQuery { pub document_type_name: String, /// `where` clauses for the query pub where_clauses: Vec, + /// Time-range (`IN_TIME_RANGE`) selections on a timestamp field covered + /// by a `timeRange` index. These are emitted as `IN_TIME_RANGE` clauses + /// on the v1 wire and resolved server-side from the current block time; + /// the verifier re-derives the same bucket from the quorum-signed + /// response metadata time. v1-only (the v0 wire has no `IN_TIME_RANGE` + /// operator). See [`Self::with_time_range`] and + /// [`Self::with_time_range_grid`]. + #[cfg_attr(feature = "mocks", serde(default))] + pub time_range_clauses: Vec, /// SQL `GROUP BY` field names, in left-to-right order. Empty = /// no explicit grouping (aggregate count for `select=Count`). /// Only meaningful when `select=Count`; non-empty with @@ -175,6 +203,7 @@ impl DocumentQuery { data_contract: Arc::clone(&contract), document_type_name: document_type_name.to_string(), where_clauses: vec![], + time_range_clauses: vec![], group_by: Vec::new(), having: Vec::new(), order_by_clauses: vec![], @@ -185,8 +214,14 @@ impl DocumentQuery { } /// Create new document query based on a [DriveDocumentQuery]. - pub fn new_with_drive_query(d: &DriveDocumentQuery) -> Self { - Self::from(d) + /// + /// Fails when the drive query carries time-range resolution provenance + /// (`resolved_time_ranges`): the resolved bucket equality cannot be + /// represented without it — see the `TryFrom` impl. Build the query + /// with [`Self::with_time_range`] / [`Self::with_time_range_grid`] + /// instead for time-range selections. + pub fn new_with_drive_query(d: &DriveDocumentQuery) -> Result { + Self::try_from(d) } /// Point to a specific document ID. @@ -209,6 +244,53 @@ impl DocumentQuery { self } + /// Restrict the query to a single time-range bucket of `field` + /// (a timestamp covered by a `timeRange` index), selecting either the + /// [`TimeRangeSelector::Newest`] or [`TimeRangeSelector::Oldest`] currently + /// active range. Emitted as an `IN_TIME_RANGE` clause on the v1 wire and + /// resolved server-side from the current block time; the proof verifier + /// re-derives the identical bucket from the quorum-signed response + /// metadata time. Requires protocol version 14+ — the first version + /// whose contract grammar hosts `timeRange` indexes. + /// + /// The bare selector is unambiguous only while exactly one grid buckets + /// `field`; when the contract declares several grids over it, use + /// [`Self::with_time_range_grid`] to name one. + /// + /// Existing time-range selections are preserved. + pub fn with_time_range( + mut self, + field: impl Into, + selector: TimeRangeSelector, + ) -> Self { + self.time_range_clauses.push(TimeRangeClause { + field: field.into(), + selector, + grid: None, + }); + self + } + + /// [`Self::with_time_range`] naming a specific grid — required when the + /// contract buckets `field` with more than one `timeRange` grid. The + /// spec's `range` / `step` / `phase` are the contract's own declared + /// seconds, verbatim. + /// + /// Existing time-range selections are preserved. + pub fn with_time_range_grid( + mut self, + field: impl Into, + selector: TimeRangeSelector, + grid: TimeRangeGridSpec, + ) -> Self { + self.time_range_clauses.push(TimeRangeClause { + field: field.into(), + selector, + grid: Some(grid), + }); + self + } + /// Add order by clause to the query. /// /// Existing order by clauses will be preserved. @@ -444,13 +526,43 @@ impl FromProof for drive_proof_verifier::types::Documents { where Self: Sized + 'a, { - let request: Self::Request = request.into(); - let drive_query: DriveDocumentQuery = + let mut request: Self::Request = request.into(); + let response: Self::Response = response.into(); + + // A time-range (`IN_TIME_RANGE`) selection is resolved to a concrete + // bucket using the **quorum-signed** response metadata time — the same + // authoritative block time the server used to resolve it — so the + // reconstructed query matches the proof exactly. Resolve (and run the + // provenance-vs-shape guard, via the one shared normalization helper + // the aggregate verifiers also use) before the `DriveDocumentQuery` + // conversion so the engine sees ordinary equality clauses. + let mut resolved_time_ranges = Vec::new(); + if !request.time_range_clauses.is_empty() { + // The generated `VersionedGrpcResponse::metadata()` handles both + // response envelopes (and any future one), so no hand-written + // version match is needed here. + use dapi_grpc::platform::VersionedGrpcResponse; + let time_ms = response + .metadata() + .map(|metadata| metadata.time_ms) + .map_err(|_| drive_proof_verifier::Error::ResponseDecodeError { + error: "time range query proof response is missing block-time metadata" + .to_string(), + })?; + resolved_time_ranges = + normalize_time_range_clauses_with_metadata_time(&mut request, time_ms)?; + } + + let mut drive_query: DriveDocumentQuery = (&request) .try_into() .map_err(|e| drive_proof_verifier::Error::RequestError { error: format!("Failed to convert DocumentQuery to DriveQuery: {}", e), })?; + // The conversion cannot recover which equalities came from resolution, + // so the provenance is carried across here; index selection reads it + // to pin the query to the index that buckets the field. + drive_query.resolved_time_ranges = resolved_time_ranges; >::maybe_from_proof_with_metadata( drive_query, @@ -462,6 +574,81 @@ impl FromProof for drive_proof_verifier::types::Documents { } } +/// Resolve a request's pending time-range (`IN_TIME_RANGE`) selections into +/// concrete bucket-equality clauses on `request.where_clauses`, using the +/// **quorum-signed** response metadata block time — the same authoritative +/// time the server used — so the reconstructed query matches the proof +/// exactly. +/// +/// Every proof-verification path that rebuilds a drive query from a +/// [`DocumentQuery`] must call this (or perform the identical resolution) +/// *before* reading `request.where_clauses` for mode detection, covering-index +/// selection, or query reconstruction: the documents path does it inline in +/// its `FromProof` impl, and the count / sum / average aggregate helpers call +/// this before resolving their mode. Skipping it would rebuild the query from +/// a different shape than the prover used. +/// +/// Returns the resolution provenance — one [`ResolvedTimeRange`] (field + +/// exact grid) per selection, whose pushed clause is a bucket equality +/// rather than a raw-timestamp one. Callers must carry them into index +/// selection (`DriveDocumentQuery::resolved_time_ranges`, or the +/// `resolved_time_ranges` argument of the aggregate index pickers): +/// the pushed clause is an ordinary equality and nothing downstream can +/// otherwise tell that it must be matched against the resolved grid's +/// bucket starts. +/// [`resolve_time_range_clauses_with_metadata_time`] followed immediately by +/// the provenance-vs-shape guard — the two-step normalization every +/// proof-verification path must run, in this order, before mode detection, +/// covering-index selection, or query reconstruction. One definition so a +/// future verifier path cannot omit either step or run them out of order: +/// the aggregate paths once omitted the resolution entirely (valid proofs +/// were rejected), and a path that resolved without the guard would let a +/// caller-provided `In`/range clause on the resolved field reach the index +/// pickers as if its raw values were bucket starts. +pub(super) fn normalize_time_range_clauses_with_metadata_time( + request: &mut DocumentQuery, + time_ms: u64, +) -> Result, drive_proof_verifier::Error> { + let resolved_time_ranges = resolve_time_range_clauses_with_metadata_time(request, time_ms)?; + validate_resolved_time_range_clause_shapes(&request.where_clauses, &resolved_time_ranges) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("invalid time range query shape: {}", e), + })?; + Ok(resolved_time_ranges) +} + +pub(super) fn resolve_time_range_clauses_with_metadata_time( + request: &mut DocumentQuery, + time_ms: u64, +) -> Result, drive_proof_verifier::Error> { + if request.time_range_clauses.is_empty() { + return Ok(Vec::new()); + } + let data_contract = Arc::clone(&request.data_contract); + let document_type = data_contract + .document_type_for_name(&request.document_type_name) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("document type not found for time range query: {}", e), + })?; + let time_range_clauses = std::mem::take(&mut request.time_range_clauses); + let mut resolved_time_ranges = Vec::with_capacity(time_range_clauses.len()); + for TimeRangeClause { + field, + selector, + grid, + } in time_range_clauses + { + let (clause, resolved) = + resolve_time_range_bucket_clause(&field, selector, grid, document_type, time_ms) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("failed to resolve time range clause: {}", e), + })?; + request.where_clauses.push(clause); + resolved_time_ranges.push(resolved); + } + Ok(resolved_time_ranges) +} + /// Version-aware encoder. The dispatch is driven by the /// `drive_abci.query.document_query` feature-version on /// [`PlatformVersion`]: `0` → V0 wire (used by v3.0 testnet), `1` → @@ -470,7 +657,8 @@ impl FromProof for drive_proof_verifier::types::Documents { /// V0 lacks `selects` / `group_by` / `having` / `offset` and the /// optional-limit semantics — callers that set those features get /// `Error::Config` with a clear "requires Platform v3.1+" message -/// rather than a silently-truncated request. +/// rather than a silently-truncated request. Time-range clauses are +/// additionally gated on the v14 contract grammar — see the `1 =>` arm. impl TryFromPlatformVersioned for GetDocumentsRequest { type Error = Error; @@ -483,6 +671,7 @@ impl TryFromPlatformVersioned for GetDocumentsRequest { data_contract, document_type_name, where_clauses, + time_range_clauses, group_by, having, order_by_clauses, @@ -505,30 +694,63 @@ impl TryFromPlatformVersioned for GetDocumentsRequest { ); match feature_version { - 0 => encode_v0( - data_contract.id().to_vec(), - document_type_name, - where_clauses, - order_by_clauses, - limit, - offset, - start, - &select, - &group_by, - &having, - ), - 1 => encode_v1( - data_contract.id().to_vec(), - document_type_name, - where_clauses, - order_by_clauses, - limit, - offset, - start, - select, - group_by, - having, - ), + 0 => { + if !time_range_clauses.is_empty() { + return Err(Error::Config( + "time range (IN_TIME_RANGE) queries require protocol version 14+; the \ + v0 getDocuments wire has no time-range operator" + .to_string(), + )); + } + encode_v0( + data_contract.id().to_vec(), + document_type_name, + where_clauses, + order_by_clauses, + limit, + offset, + start, + &select, + &group_by, + &having, + ) + } + 1 => { + // The v1 wire predates time-range indexes: protocol + // versions 12 and 13 also serve it, but their contract + // grammar (document meta-schema generations 1 and 2) + // cannot host a `timeRange` index. Gate on the grammar + // generation — the same table the server's parser reads — + // rather than emitting an operator a pre-v14 server + // rejects as an unknown discriminant. + let grammar_generation = platform_version + .dpp + .contract_versions + .document_type_versions + .schema + .document_type_schema; + if !time_range_clauses.is_empty() && grammar_generation < 3 { + return Err(Error::Config(format!( + "time range (IN_TIME_RANGE) queries require protocol version 14+ — the \ + first version whose contract grammar hosts `timeRange` indexes; this \ + network runs protocol version {}", + platform_version.protocol_version + ))); + } + encode_v1( + data_contract.id().to_vec(), + document_type_name, + where_clauses, + time_range_clauses, + order_by_clauses, + limit, + offset, + start, + select, + group_by, + having, + ) + } n => Err(Error::Config(format!( "GetDocumentsRequest wire encoder does not support feature_version={n} \ (drive_abci.query.document_query) on PlatformVersion v{}", @@ -543,6 +765,7 @@ fn encode_v1( data_contract_id: Vec, document_type: String, where_clauses: Vec, + time_range_clauses: Vec, order_by_clauses: Vec, limit: u32, offset: Option, @@ -551,10 +774,57 @@ fn encode_v1( group_by: Vec, having: Vec, ) -> Result { - let where_clauses = where_clauses + let mut where_clauses = where_clauses .into_iter() .map(where_clause_to_proto) .collect::, _>>()?; + // Append time-range selections as `IN_TIME_RANGE` clauses. A grid-less + // selection rides as the bare `"newest"`/`"oldest"` text operand; a + // grid-targeted one as the list operand `[selector, range, step]` / + // `[selector, range, step, phase]` in the contract's declared seconds + // (zero phase spelled by omission — one wire spelling per grid, the + // same rule the contract grammar and the storage key follow). The + // server resolves them to a concrete bucket from current block time; + // the verifier re-derives the same bucket from the signed response + // metadata time. + for TimeRangeClause { + field, + selector, + grid, + } in time_range_clauses + { + let selector_value = ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::Text( + selector.as_str().to_string(), + )), + }; + let operand = match grid { + None => selector_value, + Some(spec) => { + let uint = |n: u64| ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::Uint64Value(n)), + }; + let mut values = vec![ + selector_value, + uint(spec.range_seconds), + uint(spec.step_seconds), + ]; + if spec.phase_seconds != 0 { + values.push(uint(spec.phase_seconds)); + } + ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::List( + document_field_value::ValueList { values }, + )), + } + } + }; + where_clauses.push(ProtoWhereClause { + field, + operator: ProtoWhereOperator::InTimeRange as i32, + value: Some(operand), + }); + } let order_by = order_by_clauses .into_iter() .map(order_clause_to_proto) @@ -704,8 +974,28 @@ fn encode_v0( }) } -impl<'a> From<&'a DriveDocumentQuery<'a>> for DocumentQuery { - fn from(value: &'a DriveDocumentQuery<'a>) -> Self { +impl<'a> TryFrom<&'a DriveDocumentQuery<'a>> for DocumentQuery { + type Error = crate::error::Error; + + /// Fallible by necessity: a drive query carrying `resolved_time_ranges` + /// holds bucket-start equalities whose meaning lives in the provenance, + /// and `DocumentQuery` has no field to carry it — the original + /// `IN_TIME_RANGE` selector cannot be reconstructed from the resolved + /// query. Serializing such a query would silently demote the bucket + /// equality to a raw-timestamp predicate: a transformed-index-only + /// contract then rejects the request, while a contract with a competing + /// plain index returns a different — but validly proven — result. + fn try_from(value: &'a DriveDocumentQuery<'a>) -> Result { + if !value.resolved_time_ranges.is_empty() { + return Err(Error::Config( + "a drive query carrying time-range resolution provenance cannot be \ + converted to a DocumentQuery: the resolved bucket equality would be \ + demoted to a raw-timestamp predicate. Build the DocumentQuery with \ + `with_time_range` / `with_time_range_grid` instead, so the selector \ + is resolved against the signed response metadata" + .to_string(), + )); + } let data_contract = value.contract.clone(); let document_type_name = value.document_type.name(); let where_clauses = value.internal_clauses.clone().into(); @@ -722,57 +1012,32 @@ impl<'a> From<&'a DriveDocumentQuery<'a>> for DocumentQuery { None }; - Self { - // `DriveDocumentQuery` has no SELECT/GROUP BY/HAVING + Ok(Self { + // `DriveDocumentQuery` has no SELECT/GROUP BY/HAVING/time-range // concept — it's a documents-only query. Default to the // v1 documents shape. select: SelectProjection::documents(), data_contract: Arc::new(data_contract), document_type_name: document_type_name.to_string(), where_clauses, + time_range_clauses: Vec::new(), group_by: Vec::new(), having: Vec::new(), order_by_clauses, limit, offset, start, - } + }) } } -impl<'a> From> for DocumentQuery { - fn from(value: DriveDocumentQuery<'a>) -> Self { - let data_contract = value.contract.clone(); - let document_type_name = value.document_type.name(); - 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 { - true => Some(Start::StartAt(start_at.to_vec())), - false => Some(Start::StartAfter(start_at.to_vec())), - } - } else { - None - }; +impl<'a> TryFrom> for DocumentQuery { + type Error = crate::error::Error; - Self { - // `DriveDocumentQuery` has no SELECT/GROUP BY/HAVING - // concept — it's a documents-only query. Default to the - // v1 documents shape. - select: SelectProjection::documents(), - data_contract: Arc::new(data_contract), - document_type_name: document_type_name.to_string(), - where_clauses, - group_by: Vec::new(), - having: Vec::new(), - order_by_clauses, - limit, - offset, - start, - } + /// By-value twin of the by-reference conversion above — same + /// provenance rejection, same rationale. + fn try_from(value: DriveDocumentQuery<'a>) -> Result { + DocumentQuery::try_from(&value) } } @@ -780,6 +1045,22 @@ impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> { type Error = crate::error::Error; fn try_from(request: &'a DocumentQuery) -> Result { + // A pending (unresolved) time-range selection MUST be resolved into a + // concrete bucket-equality clause before a drive query can be built — + // see `resolve_time_range_clauses_with_metadata_time`. Silently + // dropping it here would rebuild (and verify against) a strictly + // broader query than the prover ran, so refuse instead: this makes + // "forgot to resolve" a loud error on every present and future call + // path rather than a silent verification hole. + if !request.time_range_clauses.is_empty() { + return Err(Error::Config( + "the query's time range (IN_TIME_RANGE) selections have not been resolved into \ + bucket equalities; resolve them against the response's quorum-signed metadata \ + time before building a drive query" + .to_string(), + )); + } + // let data_contract = request.data_contract.clone(); let document_type = request .data_contract @@ -872,6 +1153,12 @@ impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms: None, + // A `DocumentQuery` reaching here carries no unresolved + // time-range selection (rejected above) and cannot tell which of + // its equalities came from resolution. Callers that resolved + // selections assign the fields they resolved onto the returned + // query; everything else is a raw query. + resolved_time_ranges: vec![], }; Ok(query) @@ -1100,3 +1387,95 @@ fn value_to_proto_at_depth(value: Value, depth: u8) -> Result Arc { + let schemas = platform_value!({ + "post": { + "type": "object", + "properties": { + "hashtag": { "type": "string", "maxLength": 63, "position": 0 }, + }, + "required": ["hashtag"], + "additionalProperties": false, + } + }); + let contract = DataContractFactory::new(PlatformVersion::latest().protocol_version) + .expect("expected a factory") + .create_with_value_config(Identifier::new([7u8; 32]), 0, schemas, None, None) + .expect("the post contract is well-formed") + .data_contract_owned(); + Arc::new(contract) + } + + fn newest_time_range_query() -> DocumentQuery { + DocumentQuery::new(post_contract(), "post") + .expect("the fixture has this document type") + .with_time_range("$createdAt", TimeRangeSelector::Newest) + } + + #[test] + fn a_time_range_query_refuses_to_encode_for_protocol_version_13() { + let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists"); + let error = GetDocumentsRequest::try_from_platform_versioned( + newest_time_range_query(), + platform_version, + ) + .expect_err("protocol version 13's contract grammar has no timeRange indexes"); + let message = match error { + Error::Config(message) => message, + other => panic!("expected Error::Config, got {other:?}"), + }; + assert!( + message.contains("protocol version 14"), + "the refusal must name the real version floor, got: {message}" + ); + } + + #[test] + fn a_time_range_query_encodes_the_operator_for_protocol_version_14() { + let platform_version = PlatformVersion::get(14).expect("protocol version 14 exists"); + let request = GetDocumentsRequest::try_from_platform_versioned( + newest_time_range_query(), + platform_version, + ) + .expect("protocol version 14 hosts timeRange indexes"); + let Some(V1(v1)) = request.version else { + panic!("protocol version 14 encodes on the v1 wire"); + }; + let operators: Vec = v1 + .where_clauses + .iter() + .map(|clause| clause.operator) + .collect(); + assert_eq!( + operators, + vec![ProtoWhereOperator::InTimeRange as i32], + "the pending selector must ride as exactly one IN_TIME_RANGE clause" + ); + } + + #[test] + fn a_query_without_time_range_clauses_still_encodes_for_protocol_version_13() { + let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists"); + let query = DocumentQuery::new(post_contract(), "post") + .expect("the fixture has this document type"); + GetDocumentsRequest::try_from_platform_versioned(query, platform_version) + .expect("the gate only refuses queries that carry a time-range selection"); + } +} diff --git a/packages/dash-platform-queries/src/documents/having_proof_helpers.rs b/packages/dash-platform-queries/src/documents/having_proof_helpers.rs index c751e30ca09..91b2e5339d8 100644 --- a/packages/dash-platform-queries/src/documents/having_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/having_proof_helpers.rs @@ -16,6 +16,7 @@ //! //! [`DocumentHavingEntries`]: drive_proof_verifier::DocumentHavingEntries +use crate::documents::document_query::normalize_time_range_clauses_with_metadata_time; use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; @@ -88,11 +89,41 @@ pub(super) fn assert_having_shape( /// quorum-signed app hash happens inside [`verify_having_range_proof`] /// and cannot be skipped through this helper. pub(super) fn verify_having_query( - request: DocumentQuery, + mut request: DocumentQuery, response: GetDocumentsResponse, platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let proof = response + .proof() + .or(Err(drive_proof_verifier::Error::NoProofInResult))?; + let mtd = response + .metadata() + .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?; + + // Resolve any pending time-range selection through the shared + // normalization helper, then reject the request outright if anything + // resolved — mirroring the server-side rejection in drive's + // `execute_document_having_request`. HAVING accepts equality prefixes + // on compound ranked indexes (which exclude transformed indexes), so a + // resolved bucket-start equality would pin a *plain* ranked index on + // the same timestamp field, and a malicious node could return a valid + // proof over raw-timestamp matches at the bucket boundary instead of + // the requested window. Without this guard the verifier would + // authenticate a request shape honest servers refuse. + let resolved_time_ranges = + normalize_time_range_clauses_with_metadata_time(&mut request, mtd.time_ms)?; + if !resolved_time_ranges.is_empty() { + return Err(drive_proof_verifier::Error::RequestError { + error: "a HAVING query cannot carry a time-range (IN_TIME_RANGE) selection: its \ + equality prefixes pin plain ranked indexes, so a resolved bucket-start \ + equality would authenticate raw-timestamp matches at the bucket boundary \ + instead of window membership — the server rejects this request and the \ + verifier must not authenticate a proof for it" + .to_string(), + }); + } + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -102,12 +133,6 @@ pub(super) fn verify_having_query( 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_having_shape(&request, platform_version)?; diff --git a/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs b/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs index f442842fb4c..7ab5980bb60 100644 --- a/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs @@ -13,12 +13,13 @@ //! 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. +//! surface has exactly one proof primitive (grovedb's unified +//! `prove_query` over `PathQuery::new_axis_top_k`), and all of a +//! request's variation is carried *inside* the query struct. //! //! [`DocumentRankedEntries`]: drive_proof_verifier::DocumentRankedEntries +use crate::documents::document_query::normalize_time_range_clauses_with_metadata_time; use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; @@ -116,11 +117,43 @@ pub(super) fn assert_ranked_shape( /// 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, + mut request: DocumentQuery, response: GetDocumentsResponse, platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let proof = response + .proof() + .or(Err(drive_proof_verifier::Error::NoProofInResult))?; + let mtd = response + .metadata() + .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?; + + // Resolve any pending time-range (`IN_TIME_RANGE`) selections through + // the shared normalization helper, then reject the request outright if + // anything resolved — mirroring the server, whose + // `execute_document_ranked_request` refuses non-empty provenance. The + // rejection is load-bearing, not a shape formality: ranked mode accepts + // equality pins on compound ranked indexes, and the ranked picker + // excludes transformed indexes, so a resolved bucket-start equality + // would pin a *plain* ranked index on the same timestamp field. A + // malicious node could then return a valid proof over that subtree — + // documents whose raw timestamp equals the bucket boundary, not the + // requested window — and without this guard the verifier would + // authenticate it even though an honest server rejects the request. + let resolved_time_ranges = + normalize_time_range_clauses_with_metadata_time(&mut request, mtd.time_ms)?; + if !resolved_time_ranges.is_empty() { + return Err(drive_proof_verifier::Error::RequestError { + error: "a ranked query cannot carry a time-range (IN_TIME_RANGE) selection: \ + ranking groups by an index's own property, a document belongs to every \ + bucket containing its timestamp, and the resolved bucket-start equality \ + could only pin a plain index over raw timestamps — the server rejects \ + this request and the verifier must not authenticate a proof for it" + .to_string(), + }); + } + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -130,12 +163,6 @@ pub(super) fn verify_ranked_query( 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)?; diff --git a/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs index fadfd00cfd8..a91d8d83b93 100644 --- a/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs @@ -19,6 +19,7 @@ //! [`DocumentSum`]: drive_proof_verifier::DocumentSum //! [`DocumentSplitSums`]: drive_proof_verifier::DocumentSplitSums +use crate::documents::document_query::normalize_time_range_clauses_with_metadata_time; use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; @@ -33,6 +34,7 @@ use drive::query::drive_document_sum_query::index_picker::{ }; use drive::query::drive_document_sum_query::mode_detection::detect_sum_mode_from_inputs; use drive::query::drive_document_sum_query::{DocumentSumMode, DriveDocumentSumQuery, SumMode}; +use drive::query::validate_and_canonicalize_where_clauses; use drive::query::{SelectFunction, WhereOperator}; use drive_proof_verifier::{ verify_aggregate_sum_proof, verify_carrier_aggregate_sum_proof, verify_distinct_sum_proof, @@ -89,11 +91,43 @@ pub(super) fn assert_select_is_sum( /// that should be unreachable here (`prove = true`); reject as /// `RequestError` if they bubble through. pub(super) fn verify_sum_query( - request: DocumentQuery, + mut request: DocumentQuery, response: GetDocumentsResponse, platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let proof = response + .proof() + .or(Err(drive_proof_verifier::Error::NoProofInResult))?; + let mtd = response + .metadata() + .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?; + let contract_id = request.data_contract.id().to_buffer(); + let sum_property = request.select.field.clone(); + + // Resolve any pending time-range (`IN_TIME_RANGE`) selections into + // concrete bucket-equality clauses using the quorum-signed metadata + // block time — BEFORE mode detection and covering-index selection + // below, which read `request.where_clauses`; the prover routed on + // the resolved shape. + // ...and enforce the same provenance-vs-shape contract the server + // dispatchers do, through the one shared normalization helper. + let resolved_time_ranges = + normalize_time_range_clauses_with_metadata_time(&mut request, mtd.time_ms)?; + + // Canonicalize exactly as the server dispatcher does (the shared step + // in `drive::query::canonicalize`): the prover merged `[f > A, f < B]` + // pairs into one `between*` clause before its mode detection, so mode + // detection here must run over the same canonical shape or a valid + // proof is rejected. + request.where_clauses = validate_and_canonicalize_where_clauses( + std::mem::take(&mut request.where_clauses), + platform_version, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("where-clause canonicalization failed: {e}"), + })?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -103,14 +137,6 @@ pub(super) fn verify_sum_query( 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 contract_id = request.data_contract.id().to_buffer(); - let sum_property = request.select.field.clone(); // Resolve the SQL-shape `SumMode` the request implies. Same // decision tree as `validate_and_route` in the abci handler — @@ -171,6 +197,7 @@ pub(super) fn verify_sum_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_ranges, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "prove range SUM requires a `rangeSummable: true` index whose last \ @@ -183,6 +210,7 @@ pub(super) fn verify_sum_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_ranges, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "prove SUM requires a `summable: \"\"` index whose properties \ diff --git a/packages/platform-test-suite/test/e2e/contacts.spec.js b/packages/platform-test-suite/test/e2e/contacts.spec.js index 664b927495d..4689961a5d4 100644 --- a/packages/platform-test-suite/test/e2e/contacts.spec.js +++ b/packages/platform-test-suite/test/e2e/contacts.spec.js @@ -157,7 +157,11 @@ describe('e2e', () => { { where: [['$id', '==', profile.getId()]] }, ); - expect(fetchedProfile.toObject()).to.be.deep.equal(profile.toObject()); + const fetchedProfileObject = fetchedProfile.toObject(); + // Drive stamps the contract version on stored documents (protocol v14+) + delete fetchedProfileObject.$contractVersion; + + expect(fetchedProfileObject).to.be.deep.equal(profile.toObject()); }); it('should add encryption and decryption keys to the identity', async () => { @@ -256,7 +260,11 @@ describe('e2e', () => { { where: [['$id', '==', aliceProfile.getId()]] }, ); - expect(fetchedProfile.toObject()).to.be.deep.equal(aliceProfile.toObject()); + const fetchedProfileObject = fetchedProfile.toObject(); + // Drive stamps the contract version on stored documents (protocol v14+) + delete fetchedProfileObject.$contractVersion; + + expect(fetchedProfileObject).to.be.deep.equal(aliceProfile.toObject()); }); it('should be able to update her profile', async () => { @@ -279,6 +287,8 @@ describe('e2e', () => { const fetchedProfileObject = fetchedProfile.toObject(); delete fetchedProfileObject.$updatedAt; + // Drive stamps the contract version on stored documents (protocol v14+) + delete fetchedProfileObject.$contractVersion; const aliceObject = aliceProfile.toObject(); delete aliceObject.$updatedAt; @@ -311,7 +321,11 @@ describe('e2e', () => { { where: [['$id', '==', bobContactRequest.getId()]] }, ); - expect(fetchedContactRequest.toObject()).to.be.deep.equal(bobContactRequest.toObject()); + const fetchedContactRequestObject = fetchedContactRequest.toObject(); + // Drive stamps the contract version on stored documents (protocol v14+) + delete fetchedContactRequestObject.$contractVersion; + + expect(fetchedContactRequestObject).to.be.deep.equal(bobContactRequest.toObject()); }); }); @@ -336,7 +350,11 @@ describe('e2e', () => { { where: [['$id', '==', aliceContactAcceptance.getId()]] }, ); - expect(fetchedAliceContactAcceptance.toObject()).to.be.deep.equal( + const fetchedAcceptanceObject = fetchedAliceContactAcceptance.toObject(); + // Drive stamps the contract version on stored documents (protocol v14+) + delete fetchedAcceptanceObject.$contractVersion; + + expect(fetchedAcceptanceObject).to.be.deep.equal( aliceContactAcceptance.toObject(), ); }); diff --git a/packages/platform-test-suite/test/e2e/dpns.spec.js b/packages/platform-test-suite/test/e2e/dpns.spec.js index b9c6444682a..4fb56dd7e81 100644 --- a/packages/platform-test-suite/test/e2e/dpns.spec.js +++ b/packages/platform-test-suite/test/e2e/dpns.spec.js @@ -153,6 +153,7 @@ describe('DPNS', () => { delete rawDocument.$transferredAtCoreBlockHeight; delete rawDocument.$transferredAtBlockHeight; delete rawDocument.$creatorId; + delete rawDocument.$contractVersion; delete rawDocument.preorderSalt; const rawRegisteredDomain = registeredDomain.toObject(); @@ -167,6 +168,7 @@ describe('DPNS', () => { delete rawRegisteredDomain.$transferredAtCoreBlockHeight; delete rawRegisteredDomain.$transferredAtBlockHeight; delete rawRegisteredDomain.$creatorId; + delete rawRegisteredDomain.$contractVersion; delete rawRegisteredDomain.preorderSalt; expect(rawDocument).to.deep.equal(rawRegisteredDomain); @@ -187,6 +189,7 @@ describe('DPNS', () => { delete rawDocument.$transferredAtCoreBlockHeight; delete rawDocument.$transferredAtBlockHeight; delete rawDocument.$creatorId; + delete rawDocument.$contractVersion; delete rawDocument.preorderSalt; const rawRegisteredDomain = registeredDomain.toObject(); @@ -201,6 +204,7 @@ describe('DPNS', () => { delete rawRegisteredDomain.$transferredAtCoreBlockHeight; delete rawRegisteredDomain.$transferredAtBlockHeight; delete rawRegisteredDomain.$creatorId; + delete rawRegisteredDomain.$contractVersion; delete rawRegisteredDomain.preorderSalt; expect(rawDocument).to.deep.equal(rawRegisteredDomain); @@ -224,6 +228,7 @@ describe('DPNS', () => { delete rawDocument.$transferredAtCoreBlockHeight; delete rawDocument.$transferredAtBlockHeight; delete rawDocument.$creatorId; + delete rawDocument.$contractVersion; delete rawDocument.preorderSalt; const rawRegisteredDomain = registeredDomain.toObject(); @@ -238,6 +243,7 @@ describe('DPNS', () => { delete rawRegisteredDomain.$transferredAtCoreBlockHeight; delete rawRegisteredDomain.$transferredAtBlockHeight; delete rawRegisteredDomain.$creatorId; + delete rawRegisteredDomain.$contractVersion; delete rawRegisteredDomain.preorderSalt; expect(rawDocument).to.deep.equal(rawRegisteredDomain); diff --git a/packages/platform-test-suite/test/functional/platform/Document.spec.js b/packages/platform-test-suite/test/functional/platform/Document.spec.js index f30c7b4feb4..4c6f31ccbdd 100644 --- a/packages/platform-test-suite/test/functional/platform/Document.spec.js +++ b/packages/platform-test-suite/test/functional/platform/Document.spec.js @@ -25,6 +25,10 @@ const getDocumentObject = (document) => { delete documentObject.$createdAt; delete documentObject.$updatedAt; + // Drive stamps the contract version on stored documents (protocol v14+), + // so a fetched document carries it while a locally created one does not + delete documentObject.$contractVersion; + return documentObject; }; diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 0759b188069..e72bbd77413 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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", optional = true } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", 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 index 3d952b5663c..bfc75c3058d 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json @@ -1,7 +1,7 @@ { "$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), the refersTo reference keyword on identifier properties, and the requiredSince property keyword (the contract version a property is required from), 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.", + "$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), the refersTo reference keyword on identifier properties, the requiredSince property keyword (the contract version a property is required from), and the timeRange index transform, 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": { @@ -593,6 +593,35 @@ "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." + }, + "timeRange": { + "type": "object", + "properties": { + "on": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Name of the timestamp index property to bucket. Must be this index's first property and name one of the system timestamps ($createdAt, $updatedAt or $transferredAt). A timeRange index may be unique only when range equals step (non-overlapping windows) and `on` is $createdAt." + }, + "range": { + "type": "integer", + "minimum": 1, + "description": "Length of each time range window, in seconds. Must be an exact multiple of `step`." + }, + "step": { + "type": "integer", + "minimum": 1, + "description": "Interval between successive range starts, in seconds. When `range` > `step` the ranges overlap and a document is indexed under `range / step` bucket-start values, bounded by a protocol-versioned cap (24 at protocol version 14)." + }, + "phase": { + "type": "integer", + "minimum": 0, + "description": "Grid alignment phase, in seconds. Range starts are `phase + k * step`; must be strictly less than `step` (a larger value would be a redundant spelling of `phase % step`) and strictly less than one year (31536000 — a phase further out could sit past current block time on a huge step, leaving valid timestamps before the grid's first bucket). A pure alignment offset — it moves where window boundaries fall (e.g. daily windows cut at 06:00 UTC instead of midnight) and never excludes any real timestamp. Defaults to 0." + } + }, + "required": ["on", "range", "step"], + "additionalProperties": false, + "description": "Buckets the first index property's timestamp into fixed-length, regularly-spaced (possibly overlapping) time ranges. The window parameters (`range`, `step`, `phase`) are declared in seconds, since a bucket is selected from block time and the target block interval is five seconds; the stored key is the range start as a u64 millisecond timestamp, so it stays directly comparable to the source timestamp it buckets. Enables trending/leaderboard queries within the newest/oldest active range. A system-timestamp source must be listed in the document type's required fields. Several indexes may bucket the same timestamp with different grids — each grid gets its own index subtree, keyed by the property name qualified with the grid parameters. Available from protocol version 14." } }, "required": [ 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 ea612aa9ff4..c695427710e 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 @@ -192,6 +192,13 @@ pub(super) struct ParserGeneration { pub ranked_index_key_length_check: RankedIndexKeyLengthCheck, /// See [`RankedIndexStructureCheck`]. pub ranked_index_structure_check: RankedIndexStructureCheck, + + // ---- TIME RANGE: the other generation-3 addition ---- + /// Whether the index grammar admits the `timeRange` keyword. Forwarded to + /// [`Index::try_from_value_map`] exactly like `admit_ranked`: when `false` + /// the key falls through to the unknown-key arm and is rejected as any + /// pre-generation-3 node rejected it. + pub admit_time_range: bool, } /// Reject a document type whose name is not a non-empty ASCII @@ -487,6 +494,7 @@ pub(super) fn parse_document_type_core( schema_map, &flags, &properties.flattened_document_properties, + &properties.required_fields, validation_operations, )?; @@ -778,6 +786,7 @@ fn parse_indices( schema_map: &[(Value, Value)], flags: &DocumentTypeFlags, flattened_document_properties: &IndexMap, + required_fields: &BTreeSet, validation_operations: &mut impl Extend, ) -> Result<(BTreeMap, IndexLevel), ProtocolError> { // Initialize indices @@ -816,6 +825,7 @@ fn parse_indices( .map_err(consensus_or_protocol_value_error)? .as_slice(), ctx.generation.admit_ranked, + ctx.generation.admit_time_range, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -846,6 +856,95 @@ fn parse_indices( ))); } + // TIME RANGE: the source must be a millisecond + // timestamp — a system timestamp ($createdAt / + // $updatedAt / $transferredAt) or a user `Date` + // property. Structural checks (first-property, + // range % step, the uniqueness rules — unique only + // over non-overlapping windows on `$createdAt` — + // and non-contested) already happened in `Index` + // parsing; the checks here need the document schema + // or the platform version, so they live here. A + // generation without the `timeRange` grammar never + // parses a transform, so this is a no-op there. + if let Some(transform) = &index.time_range { + // The overlap factor is the number of index + // entries a single document produces on this + // index — its write amplification — so its cap + // is a versioned system limit rather than a + // structural constant: retuning it is a + // protocol-version decision, not a code edit. + // `None` means a protocol version predating + // time-range indexes, which cannot reach here + // because the keyword does not parse there. + if let Some(max_overlap_factor) = ctx + .platform_version + .system_limits + .max_time_range_overlap_factor + { + let overlap = transform.overlap_factor(); + if overlap > max_overlap_factor { + return Err(consensus_or_protocol_data_contract_error( + DataContractError::InvalidContractStructure(format!( + "timeRange overlap factor (range / step = {}) \ + exceeds the maximum of {}; a smaller window or a \ + larger step is required to bound per-document \ + index entries", + overlap, max_overlap_factor + )), + )); + } + } + let source = transform.source.as_str(); + let is_system_timestamp = matches!( + source, + property_names::CREATED_AT + | property_names::UPDATED_AT + | property_names::TRANSFERRED_AT + ); + // A system timestamp is only ever populated when + // the schema *requires* it. Without this check a + // contract could declare `timeRange.on: + // "$createdAt"` on a doctype that never sets + // $createdAt: every document would take the null + // branch, the index would hold nothing but null + // entries, and — the transform being immutable — + // the owner could never fix it. + if is_system_timestamp && !required_fields.contains(source) { + return Err(consensus_or_protocol_data_contract_error( + DataContractError::InvalidContractStructure(format!( + "timeRange.on (\"{}\") names a system timestamp the \ + document type does not require; add it to the \ + document type's required fields so documents actually \ + carry it", + source + )), + )); + } + // Only the system timestamps can be a source. A + // user property cannot: the document-schema + // grammar has no type that parses to + // `DocumentPropertyType::Date` (`type: "string"` + // with `format: "date-time"` stays `String`, and + // the meta-schema's `type` enum has no `"date"`), + // so accepting `Date`-typed user properties here + // would be a dead branch advertising a source no + // valid contract can declare. Lift this together + // with a reachable millisecond-timestamp property + // representation, not before. + if !is_system_timestamp { + return Err(consensus_or_protocol_data_contract_error( + DataContractError::InvalidContractStructure(format!( + "timeRange.on (\"{}\") must name one of the system \ + timestamps ($createdAt, $updatedAt or $transferredAt); \ + user-defined properties are not supported as a \ + time-range source", + source + )), + )); + } + } + validation_operations.extend(std::iter::once( ProtocolValidationOperation::DocumentTypeSchemaIndexValidation( index.properties.len() as u64, @@ -977,6 +1076,14 @@ fn parse_indices( // core never branches on a version. (ctx.generation.ranked_index_structure_check)(&indices)?; + // TIME RANGE: indices that share a first property may bucket it with + // different grids (or not at all) — each grid forks into its own index + // level, keyed by the property name qualified with the grid parameters + // (`TimeRangeTransform::storage_key`), so a bucketed level never shares + // a keyspace with a plain level or with another grid's level. No + // cross-index agreement rule is needed; identical grids simply share + // one level. + let index_structure = IndexLevel::try_from_indices(indices.values(), ctx.name, ctx.platform_version)?; 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 41e5da7c184..bd45f698359 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 @@ -442,6 +442,33 @@ mod tests { try_document_type_from_schema_on_version(schema, PlatformVersion::latest()) } + /// Same as [`try_document_type_from_schema`] but with `full_validation` + /// on — the index validations (the timeRange source rules among them) + /// only run on the validating parse. + fn try_document_type_from_schema_full_validation( + schema: serde_json::Value, + ) -> Result { + let platform_version = PlatformVersion::latest(); + let config = + DataContractConfig::default_for_version(platform_version).expect("config should build"); + + let value = platform_value::to_value(schema).expect("schema should convert"); + + DocumentType::try_from_schema( + Identifier::random(), + 0, + config.version(), + "msg", + value, + None, + &BTreeMap::new(), + &config, + true, + &mut vec![], + platform_version, + ) + } + fn try_document_type_from_schema_on_version( schema: serde_json::Value, platform_version: &PlatformVersion, @@ -1017,4 +1044,146 @@ mod tests { assert_eq!(properties.get("a").unwrap().required_since, None); assert!(properties.get("a").unwrap().required); } + #[test] + fn should_reject_time_range_on_user_defined_property() { + // No user property type parses to a millisecond timestamp — `type: + // "string"` with `format: "date-time"` stays `String` — so a + // user-defined time-range source must be rejected rather than + // accepted as an index that could never bucket anything meaningful. + let err = try_document_type_from_schema_full_validation(json!({ + "type": "object", + "properties": { + "eventAt": { + "type": "string", + "maxLength": 63, + "position": 0 + } + }, + "indices": [ + { + "name": "byEventTime", + "properties": [{ "eventAt": "asc" }], + "timeRange": { "on": "eventAt", "range": 21_600u64, "step": 7_200u64 } + } + ], + "required": ["eventAt"], + "additionalProperties": false + })) + .expect_err("a user-defined time-range source must be rejected"); + + assert!( + err.to_string().contains("system timestamps"), + "expected the system-timestamp restriction, got: {err}" + ); + } + + #[test] + fn should_parse_time_range_on_required_system_timestamp() { + try_document_type_from_schema_full_validation(json!({ + "type": "object", + "properties": { + "hashtag": { + "type": "string", + "maxLength": 63, + "position": 0 + } + }, + "indices": [ + { + "name": "trending", + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "timeRange": { "on": "$createdAt", "range": 21_600u64, "step": 7_200u64 } + } + ], + "required": ["$createdAt", "hashtag"], + "additionalProperties": false + })) + .expect("a required system timestamp is the supported time-range source"); + } + + /// The overlap-factor cap is a versioned system limit + /// (`SystemLimits::max_time_range_overlap_factor`), enforced at + /// registration rather than at parse; both sides of the boundary are + /// pinned here through the versioned dispatch. 24 is a day-long window + /// sliding hourly — the natural worst case the cap is sized for. + #[test] + fn should_enforce_the_versioned_time_range_overlap_factor_cap() { + let time_range_schema = |range_seconds: u64| { + json!({ + "type": "object", + "properties": { + "hashtag": { + "type": "string", + "maxLength": 63, + "position": 0 + } + }, + "indices": [ + { + "name": "trending", + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "timeRange": { "on": "$createdAt", "range": range_seconds, "step": 3_600u64 } + } + ], + "required": ["$createdAt", "hashtag"], + "additionalProperties": false + }) + }; + + try_document_type_from_schema_full_validation(time_range_schema(24 * 3_600)) + .expect("an overlap factor at the cap must register"); + + let err = try_document_type_from_schema_full_validation(time_range_schema(25 * 3_600)) + .expect_err("an overlap factor over the cap must be rejected at registration"); + assert!( + err.to_string().contains("overlap factor"), + "expected the overlap-factor rejection, got: {err}" + ); + } + + #[test] + fn should_parse_unique_time_range_index_with_non_overlapping_windows_on_created_at() { + // "one report per author per day": `range == step` makes the buckets a + // partition, and `$createdAt` is immutable, which is exactly the pair + // of conditions a unique bucketed index needs. Asserted through the + // full-validation parse so the doctype-level checks (unique-index + // limit, required system timestamp) run too. + const ONE_DAY_SECONDS: u64 = 24 * 3_600; + let document_type = try_document_type_from_schema_full_validation(json!({ + "type": "object", + "properties": { + "author": { + "type": "string", + "maxLength": 63, + "position": 0 + } + }, + "indices": [ + { + "name": "dailyReport", + "properties": [{ "$createdAt": "asc" }, { "author": "asc" }], + "unique": true, + "timeRange": { "on": "$createdAt", "range": ONE_DAY_SECONDS, "step": ONE_DAY_SECONDS } + } + ], + "required": ["$createdAt", "author"], + "additionalProperties": false + })) + .expect("a non-overlapping $createdAt bucketing may be unique"); + + let index = document_type + .as_ref() + .indexes() + .get("dailyReport") + .expect("the index should be registered") + .clone(); + assert!(index.unique); + assert_eq!( + index + .time_range + .expect("the transform should survive the schema parse") + .overlap_factor(), + 1 + ); + } } 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 86107721e1f..3d948e2c05e 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 @@ -96,10 +96,14 @@ impl DocumentTypeV1 { 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. + // therefore has no ranked key ceiling to enforce. Both + // admissions are always `false` below generation 3; see the + // shared mapping `IndexGrammarAdmissions::for_schema_generation`. admit_ranked: false, ranked_index_key_length_check: common::no_ranked_index_key_length_check, ranked_index_structure_check: common::no_ranked_index_structure_check, + // TIME RANGE: also a generation-3 keyword; not in this grammar. + admit_time_range: false, }, platform_version, ) 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 index 1f449468155..77ad1a47981 100644 --- 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 @@ -15,9 +15,10 @@ //! 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. +// Only the ranked key-length rule below names `Index`, and it is validation-only. #[cfg(feature = "validation")] use crate::data_contract::document_type::index::Index; +use crate::data_contract::document_type::index::IndexGrammarAdmissions; #[cfg(feature = "validation")] use crate::data_contract::document_type::property::DocumentPropertyType; use crate::data_contract::document_type::v2::DocumentTypeV2; @@ -255,10 +256,14 @@ fn try_from_schema_generation_3( // 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 / TIME RANGE: the keyword admissions that make this + // generation 3, read from the shared generation → admission + // mapping so the registration-cost re-parse can never drift + // from what this parser accepts. + admit_ranked: IndexGrammarAdmissions::for_schema_generation(3).ranked, ranked_index_key_length_check: RANKED_INDEX_KEY_LENGTH_CHECK, ranked_index_structure_check: validate_no_ranked_prefix_overlap, + admit_time_range: IndexGrammarAdmissions::for_schema_generation(3).time_range, }, platform_version, )?; 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 2f099c93014..168081ce4cf 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 @@ -15,6 +15,7 @@ use crate::data_contract::errors::DataContractError; use crate::ProtocolError; use anyhow::anyhow; +use crate::data_contract::document_type::property_names; use crate::data_contract::document_type::ContestedIndexResolution::MasternodeVote; #[cfg(feature = "validation")] use crate::data_contract::errors::DataContractError::RegexError; @@ -25,6 +26,9 @@ use std::sync::OnceLock; use std::{collections::BTreeMap, convert::TryFrom}; pub mod random_index; +pub mod time_range; + +pub use time_range::TimeRangeTransform; /// 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 @@ -46,6 +50,25 @@ pub const RANKED_SUMMABLE: &str = "rankedSummable"; /// costs its own secondary tree, so `rankedAverageable` adds the Avg axis and /// nothing else. pub const RANKED_AVERAGEABLE: &str = "rankedAverageable"; +/// Index-level keyword bucketing the index's first property (a millisecond +/// timestamp) into fixed-length, regularly-spaced, possibly overlapping time +/// ranges whose window is declared in seconds; a document is indexed under +/// every range containing its timestamp. See [`TimeRangeTransform`]. +/// Meta-schema v3+ (protocol version 14). +pub const TIME_RANGE: &str = "timeRange"; +/// Upper bound (exclusive) on `timeRange.phase`, in seconds: one 365-day +/// year. The phase aligns window boundaries within one step, and combined +/// with `phase < step` this cap is what makes the uncovered region before +/// the grid's first bucket (`[0, phase)` on the millisecond timeline) +/// unreachable: it stays strictly inside 1970–1971, decades before any +/// Platform block time, so a required system timestamp — which consensus +/// validates against block time — can never fall outside every window. +/// Without the cap, a sub-step phase on a huge step could sit years in the +/// future, silently unindexing valid documents and bypassing unique +/// time-range constraints until it passed. A structural rule (like +/// `range % step == 0`), not a versioned limit: it is part of what makes a +/// transform well-formed at all. +pub const MAX_TIME_RANGE_PHASE_SECONDS: u64 = 31_536_000; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] @@ -476,6 +499,72 @@ pub struct Index { /// `book/src/drive/ranked-index-examples.md` for the worked example. #[cfg_attr(feature = "serde-conversion", serde(default))] pub ranked_averageable: bool, + /// When set, the index's first property is a timestamp that is bucketed + /// into fixed-length, regularly-spaced (possibly overlapping) time + /// ranges. The stored key for that property is the range *start* (a + /// `u64` millisecond timestamp), and a single document is indexed under + /// every range whose window contains its timestamp. See + /// [`TimeRangeTransform`]. The named source must be this index's first + /// property. Part of the meta-schema-v3 grammar (protocol version 14 and + /// later). + // + // `serde(default)`: same reasoning as the ranked axes above — the key was + // added after the struct's serde shape was in the wild, so pre-existing + // JSON must still deserialize. + #[cfg_attr(feature = "serde-conversion", serde(default))] + pub time_range: Option, +} + +/// Which grammar keywords a document meta-schema generation admits for +/// indexes — the single source of the generation → admission mapping. +/// Both consumers of [`Index::try_from_value_map`]'s admission flags read +/// it: the schema parsers (`try_from_schema`'s per-generation modules) +/// and the registration-cost re-parse. Deriving the flags anywhere else +/// invites the two to drift, and then an index the validator parses is +/// billed nothing (or a rejected one is billed). +pub(crate) struct IndexGrammarAdmissions { + /// The `ranked*` keyword family (generation 3 and later). + pub(crate) ranked: bool, + /// The `timeRange` keyword (generation 3 and later). + pub(crate) time_range: bool, +} + +impl IndexGrammarAdmissions { + /// The admissions for a `document_type_schema` generation. The two + /// flags currently move together; they stay separate fields because + /// they are separate grammar admissions — a future generation may + /// admit one without the other, and then only this mapping changes. + pub(crate) fn for_schema_generation(generation: u16) -> Self { + Self { + ranked: generation >= 3, + time_range: generation >= 3, + } + } +} + +/// Whether two values of a bucketed timestamp property occupy a common +/// index slot. The stored key for such a property is the bucket *start*, +/// so two different timestamps conflict exactly when they share a +/// containing bucket (a validated unique time-range index has overlap +/// factor 1, so each timestamp has at most one). An epoch-sliver +/// timestamp produces no index entries and so shares no slot; a +/// non-timestamp value is stored under its raw key, so raw equality +/// applies. +fn bucketed_timestamps_conflict( + transform: &TimeRangeTransform, + value1: &Value, + value2: &Value, +) -> bool { + match (value1.as_integer::(), value2.as_integer::()) { + (Some(timestamp1), Some(timestamp2)) => { + let buckets2 = transform.containing_buckets(timestamp2); + transform + .containing_buckets(timestamp1) + .iter() + .any(|bucket| buckets2.contains(bucket)) + } + _ => value1 == value2, + } } impl Index { @@ -492,7 +581,12 @@ impl Index { let Some(value2) = Value::get_optional_from_map(object2, property.name.as_str()) else { return false; }; - value1 == value2 + match self.time_range.as_ref() { + Some(transform) if property.name == transform.source => { + bucketed_timestamps_conflict(transform, value1, value2) + } + _ => value1 == value2, + } }) } /// The field names of the index @@ -503,6 +597,37 @@ impl Index { .collect() } + /// The GroveDB index-level key for `property_name` at `position` in this + /// index's property list. The first property of a time-range index is + /// keyed by [`TimeRangeTransform::storage_key`] — the name qualified with + /// the grid, so several grids over one timestamp fork into sibling + /// subtrees; every other property keeps its bare name. + /// + /// Every place that turns this index's properties into GroveDB path + /// segments — contract setup, the document walkers (via `IndexLevel`, + /// which stores these same keys), query path derivation, the uniqueness + /// probe and proof verification — must derive the segment through this + /// rule, or one logical index splits into two trees. + pub fn level_key(&self, position: usize, property_name: &str) -> String { + match (&self.time_range, position) { + (Some(transform), 0) => transform.storage_key(property_name), + _ => property_name.to_string(), + } + } + + /// Name-keyed variant of [`Self::level_key`] for callers that hold a + /// property name rather than its position: the transform's source is + /// validated to be the index's first property, so matching the name + /// against `time_range.source` identifies the grid-qualified level. + pub fn level_key_for_property(&self, property_name: &str) -> String { + match &self.time_range { + Some(transform) if transform.source == property_name => { + transform.storage_key(property_name) + } + _ => property_name.to_string(), + } + } + /// Get values pub fn extract_values(&self, data: &BTreeMap) -> Vec { self.properties @@ -644,7 +769,7 @@ impl TryFrom<&[(Value, Value)]> for Index { /// `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) + Index::try_from_value_map(index_type_value_map, false, false) } } @@ -663,9 +788,16 @@ impl Index { /// 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. + /// + /// `time_range_allowed` gates the `timeRange` keyword exactly the same + /// way: it also joined the grammar at meta-schema v3 (protocol version + /// 14). The two flags are separate parameters because they are separate + /// grammar admissions — a future generation may admit one without the + /// other. pub fn try_from_value_map( index_type_value_map: &[(Value, Value)], ranked_aggregates_allowed: bool, + time_range_allowed: bool, ) -> Result { // Decouple the map // It contains properties and a unique key @@ -719,6 +851,7 @@ impl Index { let mut ranked_countable = false; let mut ranked_summable = false; let mut ranked_averageable = false; + let mut time_range: Option = None; for (key_value, value_value) in index_type_value_map { let key = key_value.to_str()?; @@ -974,6 +1107,90 @@ impl Index { "rankedAverageable value must be a boolean".to_string(), ))?; } + // `timeRange` is guarded the same way as the ranking keywords + // above: it joined the grammar at meta-schema v3, so below + // that the key falls through to the unknown-property arm. + TIME_RANGE if time_range_allowed => { + let time_range_map = + value_value + .as_map() + .ok_or(DataContractError::ValueWrongType( + "timeRange value should be a map".to_string(), + ))?; + + let mut source: Option = None; + let mut range_seconds: Option = None; + let mut step_seconds: Option = None; + let mut phase_seconds: u64 = 0; + + for (tr_key_value, tr_value) in time_range_map { + let tr_key = tr_key_value + .to_str() + .map_err(|e| DataContractError::ValueDecodingError(e.to_string()))?; + match tr_key { + "on" => { + source = Some( + tr_value + .as_text() + .ok_or(DataContractError::ValueWrongType( + "timeRange.on should be a string".to_string(), + ))? + .to_owned(), + ); + } + "range" => { + range_seconds = Some(tr_value.to_integer().map_err(|_| { + DataContractError::ValueWrongType( + "timeRange.range should be an integer".to_string(), + ) + })?); + } + "step" => { + step_seconds = Some(tr_value.to_integer().map_err(|_| { + DataContractError::ValueWrongType( + "timeRange.step should be an integer".to_string(), + ) + })?); + } + "phase" => { + phase_seconds = tr_value.to_integer().map_err(|_| { + DataContractError::ValueWrongType( + "timeRange.phase should be an integer".to_string(), + ) + })?; + } + other => { + return Err(DataContractError::InvalidContractStructure(format!( + "unexpected timeRange field: {}", + other + ))); + } + } + } + + let source = source.ok_or(DataContractError::InvalidContractStructure( + "timeRange requires an `on` field naming the source timestamp property" + .to_string(), + ))?; + let range_seconds = + range_seconds.ok_or(DataContractError::InvalidContractStructure( + "timeRange requires a `range` field (range length in seconds)" + .to_string(), + ))?; + let step_seconds = + step_seconds.ok_or(DataContractError::InvalidContractStructure( + "timeRange requires a `step` field (interval between range starts in \ + seconds)" + .to_string(), + ))?; + + time_range = Some(TimeRangeTransform { + source, + range_seconds, + step_seconds, + phase_seconds, + }); + } "properties" => { let properties = value_value @@ -1221,6 +1438,192 @@ impl Index { )); } + // A time-range transform buckets the index's *first* property. Validate + // the structural constraints that don't need document-type context here + // (the source must be a timestamp/Date field — which does need the + // schema — is checked in `try_from_schema`). Uniqueness is admitted + // only for the narrow shape where it has a meaning: non-overlapping + // windows over an immutable timestamp. + if let Some(transform) = &time_range { + // Uniqueness and bucketing only compose when the windows + // *partition* time. With `range == step` (overlap factor 1) every + // document lands in exactly one bucket, so "at most one document + // per window per remaining key tuple" is a coherent constraint — + // one report per author per day. With `range > step` a single + // document is indexed under `range / step` bucket keys at once, so + // it would occupy the unique slot of several windows while two + // documents with different timestamps would collide in the windows + // they happen to share: there is no constraint left to enforce. + // + // Compared before the zero-step / multiple checks below because it + // only reads the declared numbers; a malformed transform still + // gets its own, more specific rejection there. + if unique && transform.range_seconds != transform.step_seconds { + return Err(DataContractError::InvalidContractStructure( + "a timeRange index cannot be unique unless range equals step \ + (non-overlapping windows): with overlapping ranges one document is indexed \ + under several bucket keys at once, which uniqueness cannot express" + .to_string(), + )); + } + // Non-overlapping windows are still only safe over an *immutable* + // source. Uniqueness validation probes the bucket the candidate + // document's timestamp falls into and leans on `allow_original`: + // a document may keep occupying its own slot unless one of the + // index's values changed, in which case the tuple moved and the + // new one must be free. `$createdAt` never changes across a + // revision, so the bucket component is fixed and the tuple can + // only move through the index's other properties — exactly the + // changes the uniqueness request reports. `$updatedAt` / + // `$transferredAt` change on *every* revision, silently migrating + // the document to a new bucket; validating that would need the old + // bucket alongside the new one to know which slot is being + // vacated, and the uniqueness request carries only the new + // timestamps. Rejected here rather than half-checked; liftable + // once the request plumbs the previous timestamp through. + if unique && transform.source != property_names::CREATED_AT { + return Err(DataContractError::InvalidContractStructure(format!( + "a unique timeRange index requires \"{}\" as its source, not \"{}\": \ + \"{}\" is immutable across updates, so a document's bucket is fixed and \ + the uniqueness tuple only moves when the index's other properties change. \ + A mutable timestamp source would move the document to a new bucket on \ + every revision, which uniqueness validation cannot check without tracking \ + the old bucket", + property_names::CREATED_AT, + transform.source, + property_names::CREATED_AT + ))); + } + if contested_index.is_some() { + return Err(DataContractError::InvalidContractStructure( + "a timeRange index cannot be a contested resource".to_string(), + )); + } + // The ranked query surface excludes bucketed indexes — a document + // is stored once per containing bucket, so ranking groups keyed by + // bucket starts would score each document `overlap_factor` times. + // Since no ranked query can ever select such an index, allowing + // the flags would only make the contract pay for ranked + // secondaries that are unreachable; reject the combination until + // bucket-aware ranked semantics are deliberately designed. + if ranked_countable || ranked_summable || ranked_averageable { + return Err(DataContractError::InvalidContractStructure( + "a timeRange index cannot be ranked (rankedCountable / rankedSummable / \ + rankedAverageable): ranked queries have no time-bucket semantics, so the \ + ranked secondaries would be maintained but never servable" + .to_string(), + )); + } + // Same reasoning as the ranked `nullSearchable` rejection above, + // plus a write-path invariant: the insert, delete and update + // walkers all agree that a null timestamp keeps a single ordinary + // null entry with its real reference. `nullSearchable: false` + // would suppress that reference on insert while the set-diff + // update path maintains it, so the combination is rejected. + if !null_searchable { + return Err(DataContractError::InvalidContractStructure( + "a timeRange index is not supported with nullSearchable: false: documents \ + with a null timestamp keep a single ordinary null entry; leave \ + nullSearchable at its default (true)" + .to_string(), + )); + } + // The window is declared in seconds but every quantity it is + // measured against — the source timestamps, the bucket starts, the + // stored index keys — is a millisecond timestamp, so a parameter is + // only usable if scaling it by 1_000 still fits in a `u64`. + // Rejecting the unscalable ones here is what lets the transform's + // `*_ms` accessors saturate instead of returning a `Result` no + // validated contract could ever trip. + for (field, seconds) in [ + ("range", transform.range_seconds), + ("step", transform.step_seconds), + ("phase", transform.phase_seconds), + ] { + if seconds > u64::MAX / 1_000 { + return Err(DataContractError::InvalidContractStructure(format!( + "timeRange.{} ({} seconds) is too large to express in milliseconds", + field, seconds + ))); + } + } + if transform.step_seconds == 0 { + return Err(DataContractError::InvalidContractStructure( + "timeRange.step must be greater than zero".to_string(), + )); + } + // The phase is a pure alignment offset: shifting the grid by a + // whole number of steps reproduces the identical grid, so any + // value >= step is a second spelling of a smaller phase. One + // grid, one spelling — reject rather than normalize, so the + // contract, the transform and the storage key all carry the same + // number. + if transform.phase_seconds >= transform.step_seconds { + return Err(DataContractError::InvalidContractStructure(format!( + "timeRange.phase ({} seconds) must be less than timeRange.step ({} \ + seconds): the phase only aligns the grid within one step, and a larger \ + value would be a redundant spelling of phase % step", + transform.phase_seconds, transform.step_seconds + ))); + } + // `phase < step` alone is not enough: the region `[0, phase)` is + // outside every window (bucket starts are `phase + k*step`, + // k >= 0), and with a huge step the phase — while still a valid + // sub-step alignment — could land years in the future (e.g. + // `step = 2_000_000_000s`, `phase = 1_900_000_000s` ≈ 2030), + // leaving every present-day timestamp without index entries and + // silently bypassing unique constraints until the phase passes. + // Capping the phase at one year keeps the uncovered region + // strictly inside 1970–1971 — decades before any Platform block + // time, and `$createdAt` & co are consensus-validated against + // block time — so no valid document timestamp can ever precede + // the first bucket. One year covers every alignment use case + // (time-of-day, weekday, month and year boundaries, timezones). + if transform.phase_seconds >= MAX_TIME_RANGE_PHASE_SECONDS { + return Err(DataContractError::InvalidContractStructure(format!( + "timeRange.phase ({} seconds) must be less than one year ({} seconds): \ + the phase aligns window boundaries, and a larger value would leave \ + valid document timestamps before the grid's first bucket, outside \ + every window", + transform.phase_seconds, MAX_TIME_RANGE_PHASE_SECONDS + ))); + } + if transform.range_seconds == 0 { + return Err(DataContractError::InvalidContractStructure( + "timeRange.range must be greater than zero".to_string(), + )); + } + if transform.range_seconds % transform.step_seconds != 0 { + return Err(DataContractError::InvalidContractStructure( + "timeRange.range must be an exact multiple of timeRange.step so the number \ + of overlapping ranges per document is deterministic" + .to_string(), + )); + } + // The overlap-factor cap is NOT checked here: it is a versioned + // system limit (`SystemLimits::max_time_range_overlap_factor`), + // and this parser has no platform version. It is enforced at + // contract registration in `try_from_schema`, like the other + // versioned index limits; this function keeps only the structural + // rules that hold at every version. + match index_properties.first() { + Some(first) if first.name == transform.source => {} + Some(first) => { + return Err(DataContractError::InvalidContractStructure(format!( + "timeRange.on (\"{}\") must name the first index property (\"{}\"); a \ + time range partitions the index by time, so it has to be the leading \ + property", + transform.source, first.name + ))); + } + None => { + return Err(DataContractError::InvalidContractStructure( + "an index with a timeRange must have at least one property".to_string(), + )); + } + } + } + // If the index didn't have a name, derive one deterministically from // its properties and their directions. Every document meta-schema // (v0/v1/v2) requires `name`, so an unnamed index can only reach this @@ -1265,6 +1668,7 @@ impl Index { ranked_countable, ranked_summable, ranked_averageable, + time_range, }) } } @@ -1335,9 +1739,435 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } + // ----------------------------------------------------------------------- + // timeRange parsing + structural validation tests + // ----------------------------------------------------------------------- + + /// `time_range` is `(on, range, step)` with the window parameters in the + /// seconds the contract grammar declares them in. + fn index_value_map( + first_property: &str, + time_range: Option<(&str, u64, u64)>, + ) -> Vec<(Value, Value)> { + let property = Value::Map(vec![( + Value::Text(first_property.to_string()), + Value::Text("asc".to_string()), + )]); + let hashtag = Value::Map(vec![( + Value::Text("hashtag".to_string()), + Value::Text("asc".to_string()), + )]); + + let mut map = vec![ + ( + Value::Text("name".to_string()), + Value::Text("trending".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![property, hashtag]), + ), + ]; + + if let Some((on, range, step)) = time_range { + map.push(( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + (Value::Text("on".to_string()), Value::Text(on.to_string())), + (Value::Text("range".to_string()), Value::U64(range)), + (Value::Text("step".to_string()), Value::U64(step)), + ]), + )); + } + + map + } + + /// [`index_value_map`] with one extra `timeRange` key — for the `phase` + /// tests and for pinning that removed keys (like the pre-phase-era + /// `origin`) fall through to the unknown-field rejection. + fn index_value_map_with_extra_time_range_key( + range: u64, + step: u64, + key: &str, + value: u64, + ) -> Vec<(Value, Value)> { + let mut map = index_value_map("$createdAt", Some(("$createdAt", range, step))); + let Some((_, Value::Map(time_range))) = map + .iter_mut() + .find(|(k, _)| k.as_text() == Some("timeRange")) + else { + panic!("the helper always builds a timeRange map"); + }; + time_range.push((Value::Text(key.to_string()), Value::U64(value))); + map + } + + /// `phase` parses into the transform, and its being strictly less than + /// `step` is a structural rule — a larger value is a redundant spelling + /// of `phase % step` and is rejected rather than normalized, so the + /// contract, the transform and the storage key all carry one number. + #[test] + fn time_range_phase_parses_and_must_be_less_than_step() { + let map = index_value_map_with_extra_time_range_key(21_600, 7_200, "phase", 3_600); + let index = Index::try_from_value_map(map.as_slice(), false, true).expect("should parse"); + let transform = index.time_range.expect("time_range should be set"); + assert_eq!(transform.phase_seconds, 3_600); + + // phase == step: one whole step of shift reproduces the identical + // grid, so this is the smallest redundant spelling. + let map = index_value_map_with_extra_time_range_key(21_600, 7_200, "phase", 7_200); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + + let map = index_value_map_with_extra_time_range_key(21_600, 7_200, "phase", 10_000); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + /// `phase < step` alone is not enough: on a huge step a sub-step phase + /// can sit years in the *future*, leaving every present-day timestamp + /// before the grid's first bucket — unindexed, and free to bypass a + /// unique constraint. The one-year cap closes that: the uncovered + /// region stays inside 1970–1971, unreachable for consensus-validated + /// timestamps. + #[test] + fn time_range_phase_is_capped_at_one_year_even_when_the_step_allows_more() { + // The reported exploit shape: overlap factor 1, everything scalable, + // phase < step — but the phase anchor lands around 2030, so a unique + // index would silently skip every document until then. + let map = index_value_map_with_extra_time_range_key( + 2_000_000_000, + 2_000_000_000, + "phase", + 1_900_000_000, + ); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + + // Just under the cap on the same huge step parses fine. + let map = index_value_map_with_extra_time_range_key( + 2_000_000_000, + 2_000_000_000, + "phase", + MAX_TIME_RANGE_PHASE_SECONDS - 1, + ); + let index = Index::try_from_value_map(map.as_slice(), false, true).expect("should parse"); + assert_eq!( + index.time_range.expect("transform set").phase_seconds, + MAX_TIME_RANGE_PHASE_SECONDS - 1 + ); + + // Exactly the cap is rejected (exclusive bound). + let map = index_value_map_with_extra_time_range_key( + 2_000_000_000, + 2_000_000_000, + "phase", + MAX_TIME_RANGE_PHASE_SECONDS, + ); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + /// The pre-phase grammar's `origin` key no longer exists: it named an + /// absolute grid anchor with beginning-of-time semantics the phase-only + /// design dropped, so it must be rejected as an unknown field rather + /// than silently ignored. + #[test] + fn time_range_rejects_the_removed_origin_key() { + let map = index_value_map_with_extra_time_range_key(21_600, 7_200, "origin", 3_600); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + /// Two grids over the same property produce distinct storage keys — the + /// fork that lets them coexist as sibling index subtrees — and identical + /// grids produce the identical key, so indexes sharing a grid share a + /// level. Zero phase is spelled by omission; a declared phase appends + /// the fourth part. + #[test] + fn time_range_storage_keys_are_grid_qualified() { + let map = index_value_map("$createdAt", Some(("$createdAt", 21_600, 7_200))); + let index = Index::try_from_value_map(map.as_slice(), false, true).expect("should parse"); + assert_eq!(index.level_key(0, "$createdAt"), "$createdAt#21600#7200"); + assert_eq!( + index.level_key_for_property("$createdAt"), + "$createdAt#21600#7200" + ); + // non-first / non-source properties keep their bare names + assert_eq!(index.level_key(1, "hashtag"), "hashtag"); + assert_eq!(index.level_key_for_property("hashtag"), "hashtag"); + + let map = index_value_map_with_extra_time_range_key(21_600, 7_200, "phase", 3_600); + let phased = Index::try_from_value_map(map.as_slice(), false, true).expect("should parse"); + assert_eq!( + phased.level_key(0, "$createdAt"), + "$createdAt#21600#7200#3600" + ); + assert_ne!( + phased.level_key(0, "$createdAt"), + index.level_key(0, "$createdAt"), + "different grids must fork into different levels" + ); + } + + #[test] + fn time_range_index_parses() { + // A six-hour window refreshed every two hours. + let map = index_value_map("$createdAt", Some(("$createdAt", 21_600, 7_200))); + let index = Index::try_from_value_map(map.as_slice(), false, true).expect("should parse"); + let transform = index.time_range.expect("time_range should be set"); + assert_eq!(transform.source, "$createdAt"); + assert_eq!(transform.range_seconds, 21_600); + assert_eq!(transform.step_seconds, 7_200); + assert_eq!(transform.phase_seconds, 0); + assert_eq!(transform.overlap_factor(), 3); + // The parsed seconds are what the bucket math scales into the + // millisecond domain the source timestamps live in. + assert_eq!(transform.range_ms(), 21_600_000); + assert_eq!(transform.step_ms(), 7_200_000); + } + + #[test] + fn time_range_rejects_non_multiple_range() { + let map = index_value_map("$createdAt", Some(("$createdAt", 21_600, 7_000))); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + #[test] + fn time_range_rejects_parameters_that_cannot_be_expressed_in_milliseconds() { + // `range == step` keeps the overlap factor at 1 and the multiple check + // satisfied, so the millisecond-expressibility guard is the only rule + // that can reject this transform. + let too_large = u64::MAX / 1_000 + 1; + let map = index_value_map("$createdAt", Some(("$createdAt", too_large, too_large))); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + match err { + DataContractError::InvalidContractStructure(message) => { + assert!( + message.contains("too large to express in milliseconds"), + "expected the millisecond-expressibility rejection, got: {message}" + ); + } + other => panic!("expected InvalidContractStructure, got: {other:?}"), + } + } + + #[test] + fn time_range_parse_accepts_any_overlap_factor() { + // The overlap-factor cap is a versioned system limit enforced at + // contract registration (`try_from_schema`), not a structural parse + // rule — this parser has no platform version to read it from. A + // huge factor must therefore parse; registration is what rejects it. + let map = index_value_map("$createdAt", Some(("$createdAt", 300, 1))); + let index = Index::try_from_value_map(map.as_slice(), false, true) + .expect("the parser applies structural rules only"); + assert_eq!( + index + .time_range + .expect("time_range should be set") + .overlap_factor(), + 300 + ); + } + + #[test] + fn time_range_rejects_source_not_first_property() { + // `on` names a property that is not the first index property + let map = index_value_map("$createdAt", Some(("hashtag", 60, 20))); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + #[test] + fn time_range_rejects_zero_step() { + let map = index_value_map("$createdAt", Some(("$createdAt", 60, 0))); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + #[test] + fn time_range_rejects_null_searchable_false() { + let mut map = index_value_map("$createdAt", Some(("$createdAt", 21_600, 7_200))); + map.push(( + Value::Text("nullSearchable".to_string()), + Value::Bool(false), + )); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + assert!(matches!( + err, + DataContractError::InvalidContractStructure(_) + )); + } + + #[test] + fn time_range_rejects_ranked_flags() { + // Ranked queries exclude bucketed indexes, so the combination would + // maintain ranked secondaries no query can ever select. The index is + // otherwise a fully valid ranked shape (single property, countable + + // rangeCountable), so the ranked-prerequisite checks pass and the + // rejection under test is the one that fires. + let property = Value::Map(vec![( + Value::Text("$createdAt".to_string()), + Value::Text("asc".to_string()), + )]); + let map = vec![ + ( + Value::Text("name".to_string()), + Value::Text("busiestBuckets".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![property]), + ), + (Value::Text("countable".to_string()), Value::Bool(true)), + (Value::Text("rangeCountable".to_string()), Value::Bool(true)), + ( + Value::Text("rankedCountable".to_string()), + Value::Bool(true), + ), + ( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + (Value::Text("range".to_string()), Value::U64(21_600)), + (Value::Text("step".to_string()), Value::U64(7_200)), + ]), + ), + ]; + let err = Index::try_from_value_map(map.as_slice(), true, true).unwrap_err(); + match err { + DataContractError::InvalidContractStructure(message) => { + assert!( + message.contains("cannot be ranked"), + "expected the timeRange-vs-ranked rejection, got: {message}" + ); + } + other => panic!("expected InvalidContractStructure, got: {other:?}"), + } + } + + /// One day as the contract grammar declares a window (seconds), aligned to + /// the epoch: `range == step`, so the windows tile time without + /// overlapping and every document lands in exactly one. + const ONE_DAY_SECONDS: u64 = 24 * 3_600; + + #[test] + fn unique_time_range_index_parses_for_non_overlapping_windows_on_created_at() { + // "one post per author per day": the bucket is a genuine partition of + // time, so at most one document per (day, hashtag) is a constraint the + // storage layout can actually hold. + let mut map = index_value_map( + "$createdAt", + Some(("$createdAt", ONE_DAY_SECONDS, ONE_DAY_SECONDS)), + ); + map.push((Value::Text("unique".to_string()), Value::Bool(true))); + let index = Index::try_from_value_map(map.as_slice(), false, true).expect("should parse"); + assert!(index.unique, "the unique flag must survive parsing"); + let transform = index.time_range.expect("time_range should be set"); + assert_eq!(transform.source, "$createdAt"); + assert_eq!( + transform.overlap_factor(), + 1, + "range == step is what makes the index a partition" + ); + } + + #[test] + fn time_range_rejects_unique_when_windows_overlap() { + // range = 3 * step: each document is indexed under three bucket keys + // at once, so there is no single slot for uniqueness to guard. + let mut map = index_value_map( + "$createdAt", + Some(("$createdAt", 3 * ONE_DAY_SECONDS, ONE_DAY_SECONDS)), + ); + map.push((Value::Text("unique".to_string()), Value::Bool(true))); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + match err { + DataContractError::InvalidContractStructure(message) => { + assert!( + message.contains("unique") && message.contains("non-overlapping"), + "expected the overlapping-windows rejection, got: {message}" + ); + } + other => panic!("expected InvalidContractStructure, got: {other:?}"), + } + } + + #[test] + fn time_range_rejects_unique_on_a_mutable_timestamp_source() { + // `$updatedAt` moves the document to a new bucket on every revision; + // uniqueness validation only sees the new timestamp, so it could never + // tell which bucket the document is vacating. + let mut map = index_value_map( + "$updatedAt", + Some(("$updatedAt", ONE_DAY_SECONDS, ONE_DAY_SECONDS)), + ); + map.push((Value::Text("unique".to_string()), Value::Bool(true))); + let err = Index::try_from_value_map(map.as_slice(), false, true).unwrap_err(); + match err { + DataContractError::InvalidContractStructure(message) => { + assert!( + message.contains("$createdAt"), + "expected the immutable-source requirement, got: {message}" + ); + } + other => panic!("expected InvalidContractStructure, got: {other:?}"), + } + // The same shape without `unique` is fine — the restriction is about + // uniqueness, not about bucketing `$updatedAt`. + let map = index_value_map( + "$updatedAt", + Some(("$updatedAt", ONE_DAY_SECONDS, ONE_DAY_SECONDS)), + ); + Index::try_from_value_map(map.as_slice(), false, true) + .expect("a non-unique $updatedAt bucketing stays legal"); + } + + #[test] + fn time_range_is_not_part_of_the_pre_v3_grammar() { + // Without the meta-schema-v3 grammar admission, `timeRange` falls + // through to the unknown-key arm — exactly how a pre-PV14 node + // rejected it. + let map = index_value_map("$createdAt", Some(("$createdAt", 21_600, 7_200))); + let err = Index::try_from(map.as_slice()).unwrap_err(); + assert!(matches!(err, DataContractError::ValueWrongType(_))); + let err = Index::try_from_value_map(map.as_slice(), true, false).unwrap_err(); + assert!(matches!(err, DataContractError::ValueWrongType(_))); + } + // ----------------------------------------------------------------------- // ContestedIndexResolution tests // ----------------------------------------------------------------------- @@ -1541,6 +2371,76 @@ mod tests { assert!(!index.objects_are_conflicting(&obj1, &obj2)); } + /// A daily unique grid (`range == step`, the only shape a unique + /// time-range index may take): the bucketed property occupies its + /// bucket-start slot, so conflict follows the bucket, not the raw + /// timestamp. + fn make_unique_daily_index() -> Index { + let mut index = make_index("idx", vec![("$createdAt", true), ("author", true)], true); + index.time_range = Some(TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 86_400, + step_seconds: 86_400, + phase_seconds: 0, + }); + index + } + + fn created_at_author(timestamp_ms: u64, author: &str) -> ValueMap { + vec![ + ( + Value::Text("$createdAt".to_string()), + Value::U64(timestamp_ms), + ), + ( + Value::Text("author".to_string()), + Value::Text(author.to_string()), + ), + ] + } + + const DAY_MS: u64 = 86_400_000; + + #[test] + fn test_objects_are_conflicting_time_range_same_bucket() { + let index = make_unique_daily_index(); + // different timestamps, same daily bucket, same suffix → same slot + let obj1 = created_at_author(3 * DAY_MS + 1_000, "sam"); + let obj2 = created_at_author(3 * DAY_MS + 2_000, "sam"); + assert!(index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_time_range_adjacent_buckets() { + let index = make_unique_daily_index(); + // last ms of day 3 vs first ms of day 4: adjacent slots, no conflict + let obj1 = created_at_author(4 * DAY_MS - 1, "sam"); + let obj2 = created_at_author(4 * DAY_MS, "sam"); + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_time_range_same_bucket_different_suffix() { + let index = make_unique_daily_index(); + let obj1 = created_at_author(3 * DAY_MS + 1_000, "sam"); + let obj2 = created_at_author(3 * DAY_MS + 2_000, "alice"); + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_time_range_epoch_sliver_never_conflicts() { + let mut index = make_unique_daily_index(); + // a one-hour phase leaves timestamps below the anchor with no + // index entries at all — no slot exists to share, even for + // identical timestamps + if let Some(transform) = index.time_range.as_mut() { + transform.phase_seconds = 3_600; + } + let obj1 = created_at_author(1_000, "sam"); + let obj2 = created_at_author(1_000, "sam"); + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + #[test] fn test_objects_are_conflicting_one_missing_property() { let index = make_index("idx", vec![("name", true)], true); @@ -2237,7 +3137,7 @@ mod tests { ("rankedSummable", Value::Bool(true)), ("rankedAverageable", Value::Bool(true)), ]); - let index = Index::try_from_value_map(index_map.as_slice(), true) + let index = Index::try_from_value_map(index_map.as_slice(), true, true) .expect("all three ranked keywords must parse when the grammar allows them"); assert!(index.ranked_countable); assert!(index.ranked_summable); @@ -2255,7 +3155,7 @@ mod tests { ("averageable", Value::Text("score".to_string())), ("rangeAverageable", Value::Bool(true)), ]); - let index = Index::try_from_value_map(index_map.as_slice(), true) + let index = Index::try_from_value_map(index_map.as_slice(), true, true) .expect("index without ranked keywords must parse"); assert!(!index.ranked_countable); assert!(!index.ranked_summable); @@ -2289,7 +3189,7 @@ mod tests { Value::Text("asc".to_string()), )]), ]); - let index = Index::try_from_value_map(index_map.as_slice(), true) + let index = Index::try_from_value_map(index_map.as_slice(), true, true) .expect("ranked flags on a compound index must be accepted"); assert!(index.ranked_averageable); assert_eq!(index.properties.len(), 2); @@ -2311,7 +3211,7 @@ mod tests { Value::Text("asc".to_string()), )]), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "rankedCountable without rangeCountable must be rejected on a compound index too" @@ -2334,7 +3234,7 @@ mod tests { ("rangeAverageable", Value::Bool(true)), ("rankedAverageable", Value::Bool(true)), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "ranked flags on a unique index must be rejected" @@ -2355,7 +3255,7 @@ mod tests { ("countable", Value::Text("countable".to_string())), ("rankedCountable", Value::Bool(true)), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "rankedCountable without rangeCountable must be rejected" @@ -2373,7 +3273,7 @@ mod tests { ("summable", Value::Text("score".to_string())), ("rankedSummable", Value::Bool(true)), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "rankedSummable without rangeSummable must be rejected" @@ -2393,7 +3293,7 @@ mod tests { ("rangeCountable", Value::Bool(true)), ("rankedAverageable", Value::Bool(true)), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "rankedAverageable with only the count range axis must be rejected" @@ -2413,7 +3313,7 @@ mod tests { ("rangeSummable", Value::Bool(true)), ("rankedAverageable", Value::Bool(true)), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "rankedAverageable with only the sum range axis must be rejected" @@ -2435,7 +3335,7 @@ mod tests { ("rangeAverageable", Value::Bool(true)), ("rankedAverageable", Value::Bool(true)), ]); - let index = Index::try_from_value_map(index_map.as_slice(), true) + let index = Index::try_from_value_map(index_map.as_slice(), true, true) .expect("rankedAverageable on the averageable sugar form must parse"); assert!(index.ranked_averageable); assert!(index.countable.is_countable()); @@ -2463,7 +3363,7 @@ mod tests { ("rangeSummable", Value::Bool(true)), ("rankedAverageable", Value::Bool(true)), ]); - let index = Index::try_from_value_map(index_map.as_slice(), true) + let index = Index::try_from_value_map(index_map.as_slice(), true, true) .expect("rankedAverageable on the explicit longhand form must parse"); assert!(index.ranked_averageable); assert!(index.range_countable); @@ -2481,7 +3381,7 @@ mod tests { ("rangeAverageable", Value::Bool(true)), (key, Value::Text("yes".to_string())), ]); - let result = Index::try_from_value_map(index_map.as_slice(), true); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!(result.is_err(), "{key} must reject a non-boolean value"); let msg = format!("{:?}", result.unwrap_err()); assert!( @@ -2505,7 +3405,7 @@ mod tests { ("rangeAverageable", Value::Bool(true)), (key, value.clone()), ]); - let result = Index::try_from_value_map(index_map.as_slice(), false); + let result = Index::try_from_value_map(index_map.as_slice(), false, false); assert!( result.is_err(), "{key}: {value:?} must be rejected when the ranked grammar is off" @@ -2578,7 +3478,7 @@ mod tests { 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); + let result = Index::try_from_value_map(index_map.as_slice(), true, true); assert!( result.is_err(), "{axis} with nullSearchable: false must be rejected" @@ -2598,7 +3498,7 @@ mod tests { 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) + let index = Index::try_from_value_map(index_map.as_slice(), true, true) .unwrap_or_else(|e| panic!("{axis} with no nullSearchable key must parse: {e:?}")); assert!( index.null_searchable, @@ -2614,9 +3514,10 @@ mod tests { 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:?}") - }); + let index = + Index::try_from_value_map(index_map.as_slice(), true, true).unwrap_or_else(|e| { + panic!("{axis} with an explicit nullSearchable: true must parse: {e:?}") + }); assert!(index.null_searchable); } } @@ -2627,7 +3528,7 @@ mod tests { #[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) + let index = Index::try_from_value_map(plain.as_slice(), true, true) .expect("nullSearchable: false on a plain index must still parse"); assert!(!index.null_searchable); @@ -2636,7 +3537,7 @@ mod tests { ("rangeAverageable", Value::Bool(true)), ("nullSearchable", Value::Bool(false)), ]); - let index = Index::try_from_value_map(aggregating.as_slice(), true) + let index = Index::try_from_value_map(aggregating.as_slice(), true, true) .expect("nullSearchable: false on a range-averageable index must still parse"); assert!(!index.null_searchable); assert!(!index.ranked_averageable); @@ -3119,6 +4020,7 @@ mod json_convertible_tests { ranked_countable: true, ranked_summable: false, ranked_averageable: true, + time_range: None, } } @@ -3148,6 +4050,7 @@ mod json_convertible_tests { "ranked_countable": true, "ranked_summable": false, "ranked_averageable": true, + "time_range": serde_json::Value::Null, }) ); let recovered = Index::from_json(json).expect("from_json"); 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 a352c935e86..a8b20234adf 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 @@ -67,6 +67,7 @@ impl Index { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }) } } diff --git a/packages/rs-dpp/src/data_contract/document_type/index/time_range.rs b/packages/rs-dpp/src/data_contract/document_type/index/time_range.rs new file mode 100644 index 00000000000..2e78e1c7ef0 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/index/time_range.rs @@ -0,0 +1,490 @@ +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// An index-level transform that buckets a timestamp index property into +/// fixed-length, regularly-spaced time ranges. +/// +/// The window is **declared in seconds** and **identified in milliseconds**. +/// A contract author writes `range` / `step` / `phase` as second counts +/// because the finest clock a bucket selection ever sees is block time, whose +/// target interval is five seconds — a window declared to the millisecond +/// would be precision the protocol cannot deliver. A time range is still +/// identified by a single `u64`: the **start time of the range** as a +/// millisecond timestamp, because the source fields (`$createdAt` &co.) are +/// millisecond timestamps and the stored index key has to stay directly +/// comparable to them. [`Self::range_ms`] and its siblings are the one place +/// the two units meet. +/// +/// Each range covers `[start, start + range)`. New ranges start every `step`, +/// on the grid `phase + k * step` — `phase` is a pure alignment offset, +/// validated to be strictly less than `step`, so the grid covers all of time +/// (bar the sub-`step` sliver at the epoch that no real timestamp can ever +/// fall into). When `range > step` the ranges overlap, so a single timestamp +/// falls into `range / step` ranges (the "overlap factor") and a document is +/// indexed under that many bucket-start values. +/// +/// The canonical use case is "trending" leaderboards: index on +/// `(timeRange($createdAt), hashtag)` with `countable`, then query a single +/// bucket. Per-hashtag counts within the bucket are served by the grouped +/// count surface, whose single `GROUP BY` field must itself carry an `In` +/// or range clause: `IN_TIME_RANGE($createdAt, "newest") AND hashtag IN +/// (candidates) GROUP BY hashtag` returns one count per candidate (the +/// client orders the returned groups), and a range predicate on the grouped +/// field with `rangeCountable: true` covers the open-ended form. A bare +/// `GROUP BY hashtag` with no clause on `hashtag` is not routable — +/// aggregate coverage requires every index property to carry an +/// `Equal`/`In`/range clause. Overlapping ranges guarantee that, at any +/// instant, there is always an active range covering a near-full `range` +/// window of history (see [`Self::oldest_active_start`]). +/// +/// Note that the *server-ordered* form (`ORDER BY COUNT(*)` — the ranked +/// query surface) cannot yet be combined with a time-range selection: ranked +/// queries accept no where clauses in this protocol version (their routing +/// deliberately has no equality-prefix support yet), so "top K by count +/// within the bucket" is served as the grouped count above with client-side +/// ordering until ranked prefix routing lands at a future protocol version. +/// +/// At the GroveDB storage layer, a transformed first property gets its own +/// index level keyed by [`Self::storage_key`] — the property name qualified +/// with the grid — so several grids over the same timestamp coexist as +/// sibling subtrees. Within a grid's subtree a bucket start is an ordinary +/// `u64` key segment (encoded exactly like a `$createdAt` value), so existing +/// index queries, count trees and proofs apply unchanged — the only novelty +/// is that one document produces several index entries. +// The serde keys deliberately match the contract grammar (`on` / `range` / +// `step` / `phase`, see the `timeRange` entry in the v3 document +// meta-schema), so a serialized `Index` round-trips into the same key set a +// contract author writes rather than a second, camelCased spelling. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +pub struct TimeRangeTransform { + /// The source timestamp index property this transform buckets. Must be + /// the first property of the index and must name one of the system + /// timestamps (`$createdAt` / `$updatedAt` / `$transferredAt`); the + /// document-schema grammar has no user property type that parses to a + /// millisecond timestamp, so user-defined sources are rejected at + /// contract validation until such a representation exists. + #[cfg_attr(feature = "serde-conversion", serde(rename = "on"))] + pub source: String, + /// Length of each range window, in seconds. Must be a positive multiple + /// of `step_seconds`. The window a document is measured against is this + /// length expressed in milliseconds ([`Self::range_ms`]), since bucket + /// starts are millisecond timestamps. + #[cfg_attr(feature = "serde-conversion", serde(rename = "range"))] + pub range_seconds: u64, + /// Interval between successive range starts, in seconds. Must be greater + /// than zero. Consecutive bucket starts are [`Self::step_ms`] apart on the + /// millisecond timeline. + #[cfg_attr(feature = "serde-conversion", serde(rename = "step"))] + pub step_seconds: u64, + /// Grid alignment phase, in seconds. Range starts are the millisecond + /// timestamps `phase_ms() + k * step_ms()` for `k = 0, 1, 2, …`. A pure + /// phase offset: contract validation requires `phase < step` AND + /// `phase < MAX_TIME_RANGE_PHASE_SECONDS` (one year), so shifting the + /// grid never excludes any real timestamp — the region before the first + /// bucket stays inside 1970–1971, unreachable for consensus-validated + /// timestamps — it only moves where the window boundaries fall (e.g. + /// daily windows cut at 06:00 UTC instead of midnight). Defaults to `0`. + #[cfg_attr(feature = "serde-conversion", serde(rename = "phase", default))] + pub phase_seconds: u64, +} + +impl TimeRangeTransform { + /// The window length on the millisecond timeline. + /// + /// The three `*_ms` accessors are the single crossing point between the + /// transform's two units: the parameters are seconds because that is the + /// resolution a contract author can meaningfully declare, while every + /// quantity the bucket math consumes or produces — a document's + /// `$createdAt`, a bucket start, a stored index key — is a millisecond + /// timestamp, so the parameters have to be scaled before they can take + /// part. + /// + /// Saturating rather than checked: contract validation rejects any + /// parameter above `u64::MAX / 1_000`, so a transform that came from a + /// validated contract can never reach the ceiling. A transform built + /// outside validation degrades into a pinned-to-the-maximum window + /// instead of panicking — the same defensive posture as the + /// `step_seconds == 0` handling below, which returns `None` / an empty + /// bucket set rather than dividing by zero. + pub fn range_ms(&self) -> u64 { + self.range_seconds.saturating_mul(1_000) + } + + /// The interval between successive range starts on the millisecond + /// timeline. See [`Self::range_ms`] for why the accessor exists and why + /// it saturates. + pub fn step_ms(&self) -> u64 { + self.step_seconds.saturating_mul(1_000) + } + + /// The grid alignment phase on the millisecond timeline — the first + /// range's start. See [`Self::range_ms`] for why the accessor exists and + /// why it saturates. + pub fn phase_ms(&self) -> u64 { + self.phase_seconds.saturating_mul(1_000) + } + + /// The number of overlapping ranges that contain any given instant, i.e. + /// the number of bucket-start values a single document is indexed under. + /// Equal to `range / step`. + /// + /// A ratio of two same-unit quantities, so it is unit-invariant and reads + /// the declared seconds directly rather than scaling both sides first. + /// + /// Returns `0` only for a malformed transform with a zero step; callers + /// constructing from a validated contract never observe that. + pub fn overlap_factor(&self) -> u64 { + if self.step_seconds == 0 { + return 0; + } + self.range_seconds / self.step_seconds + } + + /// The GroveDB index-level key for this grid over `property_name`: the + /// property name qualified with the grid parameters, so different grids + /// over the same timestamp fork into sibling subtrees instead of + /// colliding in one keyspace (every 6-hour bucket start is also a + /// 3-hour bucket start — unqualified, the two grids' entries would be + /// indistinguishable). + /// + /// **This is the single source of the key encoding.** Contract setup, + /// the insert/delete/update walkers, query path derivation, the + /// uniqueness probe and proof verification must all derive the level key + /// through this function; a second implementation that disagreed on any + /// detail would split one logical index into two trees. + /// + /// Format: `{property}#{range}#{step}` with `#{phase}` appended **iff** + /// the phase is non-zero — omitted-means-zero is canonical (nothing ever + /// writes `#0`), mirroring the contract grammar where `phase` is an + /// omittable key. The numbers are the contract-declared **seconds** + /// verbatim, which are already canonical. `#` can never appear in a + /// schema property name (`^[a-zA-Z0-9-_]{1,64}$`, dot-joined for nested + /// paths), so a qualified key can never collide with a plain property + /// level. + pub fn storage_key(&self, property_name: &str) -> String { + if self.phase_seconds == 0 { + format!( + "{}#{}#{}", + property_name, self.range_seconds, self.step_seconds + ) + } else { + format!( + "{}#{}#{}#{}", + property_name, self.range_seconds, self.step_seconds, self.phase_seconds + ) + } + } + + /// The start of the most recent range that has begun at or before the + /// millisecond timestamp `t`, i.e. the largest `phase + k * step` that is + /// `<= t`. + /// + /// Returns `None` for `t` before the phase anchor — with a validated + /// transform (`phase < step`) that is only the sub-`step` sliver at the + /// epoch, which no real timestamp reaches; the arm is defensive. (Also + /// `None` for a malformed zero-step transform, which a validated contract + /// can never carry.) + pub fn most_recent_start(&self, t: u64) -> Option { + let (step_ms, phase_ms) = (self.step_ms(), self.phase_ms()); + if step_ms == 0 || t < phase_ms { + return None; + } + let elapsed = t - phase_ms; + Some(phase_ms + (elapsed / step_ms) * step_ms) + } + + /// All bucket-start values whose range `[start, start + range)` contains + /// the millisecond timestamp `t`. This is the set of index entries a + /// document with timestamp `t` must be written under. + /// + /// The result is sorted in descending order (newest range first) and has + /// exactly [`Self::overlap_factor`] elements, except within the first + /// `range` after the epoch where fewer ranges have started. For `t` + /// before the phase anchor (the sub-`step` epoch sliver no real timestamp + /// reaches) the result is empty — insert, delete and update all share + /// this rule, keeping the index consistent. + pub fn containing_buckets(&self, t: u64) -> Vec { + let overlap = self.overlap_factor(); + if overlap == 0 { + return Vec::new(); + } + let Some(newest) = self.most_recent_start(t) else { + return Vec::new(); + }; + let (step_ms, phase_ms) = (self.step_ms(), self.phase_ms()); + (0..overlap) + .filter_map(|j| { + let offset = j.checked_mul(step_ms)?; + newest.checked_sub(offset) + }) + .filter(|start| *start >= phase_ms) + .collect() + } + + /// The start of the newest range that is active at the millisecond + /// timestamp `now` (the freshest started range). Querying this bucket + /// returns documents from the latest partial slice — between `0` and one + /// `step` of history. + /// + /// Returns `None` only when `now` predates the phase anchor (the epoch + /// sliver; unreachable for real block times). + pub fn newest_active_start(&self, now: u64) -> Option { + self.most_recent_start(now) + } + + /// The start of the oldest range still active at the millisecond timestamp + /// `now`. Its window `[start, start + range)` still contains `now`, so + /// querying this bucket returns a near-full trailing window of `~range` of + /// history (between `range - step` and `range`). This is the bucket to + /// query for "trending over the last range window". + /// + /// Returns `None` only when `now` predates the phase anchor (the epoch + /// sliver; unreachable for real block times). + pub fn oldest_active_start(&self, now: u64) -> Option { + let overlap = self.overlap_factor(); + if overlap == 0 { + return None; + } + let newest = self.most_recent_start(now)?; + let back = (overlap - 1).saturating_mul(self.step_ms()); + Some(newest.saturating_sub(back).max(self.phase_ms())) + } + + /// The set of index-entry keys a document with the given raw encoded + /// value for the bucketed property must be stored under. + /// + /// **This is the single source of truth for the fan-out rule**: the + /// insert, delete and update walkers in rs-drive must all derive their + /// entry keys through this function, or a document written by one walker + /// becomes unfindable by another (a consensus break on + /// update-after-insert). + /// + /// - An empty `raw` (the null / property-absent case) keeps the single + /// ordinary null entry every index gives null values. + /// - A decodable millisecond timestamp yields one key per containing + /// bucket — the bucket *start*, encoded exactly like the timestamp + /// itself. (A timestamp inside the sub-`step` epoch sliver before the + /// phase anchor yields no keys; no real timestamp reaches it.) + /// - A non-empty value that is not an 8-byte encoded timestamp keeps its + /// raw key, exactly as a non-time-range index would store it. + pub fn entry_keys_for_raw(&self, raw: &[u8]) -> Vec> { + use crate::data_contract::document_type::DocumentPropertyType; + if raw.is_empty() { + return vec![Vec::new()]; + } + // Gate on the exact encoded-timestamp width: `decode_date_timestamp` + // reads the first 8 bytes of any longer input, which would bucket a + // longer value by a truncated prefix instead of keeping its raw key. + let timestamp = (raw.len() == 8) + .then(|| DocumentPropertyType::decode_date_timestamp(raw)) + .flatten(); + match timestamp { + Some(timestamp) => self + .containing_buckets(timestamp) + .into_iter() + .map(DocumentPropertyType::encode_date_timestamp) + .collect(), + None => vec![raw.to_vec()], + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One hour as the transform declares it (seconds) and as every timestamp + /// below is expressed (milliseconds). + const HOUR_SECONDS: u64 = 3_600; + const HOUR_MS: u64 = 3_600_000; + + fn transform() -> TimeRangeTransform { + // range = 6h, step = 2h, phase = 0 → overlap factor 3. + TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 6 * HOUR_SECONDS, + step_seconds: 2 * HOUR_SECONDS, + phase_seconds: 0, + } + } + + #[test] + fn overlap_factor_is_range_over_step() { + assert_eq!(transform().overlap_factor(), 3); + } + + #[test] + fn seconds_that_cannot_be_scaled_saturate_rather_than_panic() { + // Contract validation refuses parameters this large, so the only way + // to build one is in code; the accessors must degrade into a pinned + // window instead of panicking on the multiplication. + let t = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: u64::MAX, + step_seconds: u64::MAX, + phase_seconds: 0, + }; + assert_eq!(t.range_ms(), u64::MAX); + assert_eq!(t.overlap_factor(), 1); + assert_eq!(t.most_recent_start(u64::MAX), Some(u64::MAX)); + } + + #[test] + fn most_recent_start_floors_to_step_multiple() { + let t = transform(); + let h = HOUR_MS; + // now = 7h → most recent start = 6h + assert_eq!(t.most_recent_start(7 * h), Some(6 * h)); + // exactly on a boundary stays put + assert_eq!(t.most_recent_start(6 * h), Some(6 * h)); + // exactly at the epoch is the first range + assert_eq!(t.most_recent_start(0), Some(0)); + } + + #[test] + fn the_epoch_sliver_before_the_phase_has_no_buckets() { + // A one-minute window stepping every twenty seconds, its grid phased + // five seconds into each step. The only timestamps outside every + // range are the first five seconds of 1970 — a sliver no real + // timestamp reaches; the math must still answer it honestly. + let t = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + phase_seconds: 5, + }; + assert_eq!(t.most_recent_start(4_999), None); + assert_eq!(t.containing_buckets(4_999), Vec::::new()); + assert_eq!(t.newest_active_start(4_999), None); + assert_eq!(t.oldest_active_start(4_999), None); + // at the phase anchor the first range starts + assert_eq!(t.most_recent_start(5_000), Some(5_000)); + assert_eq!(t.containing_buckets(5_000), vec![5_000]); + // every returned bucket actually contains the timestamp + for now in [5_000u64, 15_000, 64_999, 105_000] { + for start in t.containing_buckets(now) { + assert!(start <= now && now < start + t.range_ms()); + } + } + } + + #[test] + fn containing_buckets_are_the_overlapping_ranges() { + let t = transform(); + let h = HOUR_MS; + // doc at 7h belongs to ranges starting at 6h, 4h, 2h + assert_eq!(t.containing_buckets(7 * h), vec![6 * h, 4 * h, 2 * h]); + // every returned range actually contains the timestamp + for start in t.containing_buckets(7 * h) { + assert!(start <= 7 * h && 7 * h < start + t.range_ms()); + } + } + + #[test] + fn containing_buckets_truncate_near_the_epoch() { + let t = transform(); + let h = HOUR_MS; + // doc at 3h: ranges starting at 2h and 0h (4h start would be in future) + assert_eq!(t.containing_buckets(3 * h), vec![2 * h, 0]); + } + + #[test] + fn newest_vs_oldest_active() { + let t = transform(); + let h = HOUR_MS; + let now = 7 * h; + // newest active = freshest started range + assert_eq!(t.newest_active_start(now), Some(6 * h)); + // oldest active = covers the full trailing window + assert_eq!(t.oldest_active_start(now), Some(2 * h)); + // oldest active range still contains now + let oldest = t.oldest_active_start(now).expect("a range is active"); + assert!(oldest <= now && now < oldest + t.range_ms()); + } + + #[test] + fn entry_keys_follow_the_shared_fan_out_rule() { + use crate::data_contract::document_type::DocumentPropertyType; + let t = transform(); + let h = HOUR_MS; + // null keeps the single ordinary null entry + assert_eq!(t.entry_keys_for_raw(&[]), vec![Vec::::new()]); + // a decodable timestamp fans out into its containing buckets + let raw = DocumentPropertyType::encode_date_timestamp(7 * h); + assert_eq!( + t.entry_keys_for_raw(&raw), + vec![ + DocumentPropertyType::encode_date_timestamp(6 * h), + DocumentPropertyType::encode_date_timestamp(4 * h), + DocumentPropertyType::encode_date_timestamp(2 * h), + ] + ); + // a non-timestamp non-empty value keeps its raw key: shorter than + // the 8-byte encoding… + assert_eq!(t.entry_keys_for_raw(&[1, 2, 3]), vec![vec![1, 2, 3]]); + // …and longer too — it must NOT bucket by its first 8 bytes + let mut long = DocumentPropertyType::encode_date_timestamp(7 * h); + long.push(0xFF); + assert_eq!(t.entry_keys_for_raw(&long), vec![long.clone()]); + // a timestamp inside the epoch sliver belongs to no range: no keys + let t_phased = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + phase_seconds: 5, + }; + let raw = DocumentPropertyType::encode_date_timestamp(4_999); + assert_eq!(t_phased.entry_keys_for_raw(&raw), Vec::>::new()); + } + + #[test] + fn phase_shifts_alignment() { + let t = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + phase_seconds: 5, + }; + // starts are the 5th, 25th, 45th, ... second; now = the 50th second → + // most recent start is the 45th + assert_eq!(t.most_recent_start(50_000), Some(45_000)); + assert_eq!(t.containing_buckets(50_000), vec![45_000, 25_000, 5_000]); + } + + #[test] + fn storage_keys_qualify_the_property_with_the_grid() { + // phase 0 is spelled by omission — the canonical three-part form + assert_eq!( + transform().storage_key("$createdAt"), + "$createdAt#21600#7200" + ); + // a non-zero phase appends the fourth part + let t = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + phase_seconds: 5, + }; + assert_eq!(t.storage_key("$createdAt"), "$createdAt#60#20#5"); + // two grids over the same property produce distinct sibling keys — + // the collision the qualification exists to prevent (every 6h start + // is also a 3h start, so unqualified keys would interleave) + let six_hourly = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 6 * HOUR_SECONDS, + step_seconds: 6 * HOUR_SECONDS, + phase_seconds: 0, + }; + let three_hourly = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 3 * HOUR_SECONDS, + step_seconds: 3 * HOUR_SECONDS, + phase_seconds: 0, + }; + assert_ne!( + six_hourly.storage_key("$createdAt"), + three_hourly.storage_key("$createdAt") + ); + } +} 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 80b37ef390f..8f1d0c4a7b5 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 @@ -159,4 +159,40 @@ impl IndexLevel { None } + + /// Time-range counterpart of [`Self::find_first_countability_change`]. + /// Recursively finds the first index path where the `time_range` + /// transform differs between two `IndexLevel` trees. The transform + /// dictates how many index entries each document produces and under + /// which bucket keys, so changing it after creation would leave already + /// stored documents indexed under stale buckets — it is immutable. + /// + /// Returns `None` if the transform is the same everywhere. + #[cfg(feature = "validation")] + pub(super) fn find_first_time_range_change(&self, new: &IndexLevel) -> Option { + if self.time_range() != new.time_range() { + let fmt = |t: Option<&super::TimeRangeTransform>| match t { + Some(t) => format!( + "Some(on: {:?}, range: {}s, step: {}s, phase: {}s)", + t.source, t.range_seconds, t.step_seconds, t.phase_seconds + ), + None => "None".to_string(), + }; + return Some(format!( + "(timeRange: {} -> {})", + fmt(self.time_range()), + fmt(new.time_range()), + )); + } + + 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_time_range_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 77295ca0eae..3c96ae29951 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 @@ -7,6 +7,7 @@ use crate::consensus::basic::data_contract::DuplicateIndexError; use crate::consensus::basic::BasicError; use crate::consensus::ConsensusError; use crate::data_contract::document_type::index::IndexCountability; +use crate::data_contract::document_type::index::TimeRangeTransform; use crate::data_contract::document_type::index_level::IndexType::{ ContestedResourceIndex, NonUniqueIndex, UniqueIndex, }; @@ -121,6 +122,14 @@ pub struct IndexLevel { sub_index_levels: BTreeMap, /// did an index terminate at this level has_index_with_type: Option, + /// When set, the property reached at this level is a timestamp that is + /// bucketed into time ranges (see [`TimeRangeTransform`]). Only ever set + /// on a *first-property* node (a direct child of the root), because a + /// time-range transform must be its index's leading property. At + /// insert/delete/update time the document's timestamp for this property + /// is expanded into one key per overlapping range bucket instead of a + /// single key. Immutable after contract creation. + time_range: Option, /// unique level identifier level_identifier: u64, } @@ -134,6 +143,12 @@ impl IndexLevel { &self.sub_index_levels } + /// The time-range transform applied to the property reached at this + /// level, if any. Only set on first-property nodes. + pub fn time_range(&self) -> Option<&TimeRangeTransform> { + self.time_range.as_ref() + } + pub fn has_index_with_type(&self) -> Option<&IndexLevelTypeInfo> { // Was `Option` (Copy) before the v3 sum-tree // expansion added `summable: Option` to the struct, which @@ -235,6 +250,7 @@ impl IndexLevel { let mut index_level = IndexLevel { sub_index_levels: Default::default(), has_index_with_type: None, + time_range: None, level_identifier: 0, }; @@ -243,21 +259,39 @@ impl IndexLevel { for index_to_borrow in indices { let index = index_to_borrow.borrow(); let mut current_level = &mut index_level; - let mut properties_iter = index.properties.iter().peekable(); - - while let Some(index_part) = properties_iter.next() { + let mut properties_iter = index.properties.iter().enumerate().peekable(); + + while let Some((position, index_part)) = properties_iter.next() { + // A time-range transform always targets the index's first + // property, and its level is keyed by the property name + // *qualified with the grid* (`Index::level_key`, backed by + // `TimeRangeTransform::storage_key`) rather than the bare + // name. That fork is what lets several grids over one + // timestamp — and a plain index over the same timestamp — + // coexist: each grid's bucket starts live in their own + // subtree instead of interleaving in one keyspace. Identical + // grids map to the identical key, so indices sharing a grid + // still share the level. + let level_key = index.level_key(position, &index_part.name); current_level = current_level .sub_index_levels - .entry(index_part.name.clone()) + .entry(level_key) .or_insert_with(|| { counter += 1; IndexLevel { level_identifier: counter, sub_index_levels: Default::default(), has_index_with_type: None, + time_range: None, } }); + if position == 0 { + if let Some(transform) = &index.time_range { + current_level.time_range = Some(transform.clone()); + } + } + // The last property if properties_iter.peek().is_none() { // This level already has been initialized. @@ -399,6 +433,20 @@ impl IndexLevel { ); } + // A time-range transform determines how many index entries each + // document produces and under which bucket keys. Changing it after + // creation would leave already-stored documents indexed under stale + // buckets, so it is immutable — reject any change. + if let Some(time_range_change_path) = self.find_first_time_range_change(new_indices) { + return SimpleConsensusValidationResult::new_with_error( + DataContractInvalidIndexDefinitionUpdateError::new( + document_type_name.to_string(), + time_range_change_path, + ) + .into(), + ); + } + SimpleConsensusValidationResult::new() } } @@ -430,6 +478,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -464,6 +513,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![ @@ -483,6 +533,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, Index { name: "test2".to_string(), @@ -500,6 +551,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, ]; @@ -543,6 +595,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, Index { name: "test2".to_string(), @@ -560,6 +613,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, ]; @@ -579,6 +633,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -620,6 +675,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -644,6 +700,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -691,6 +748,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -709,6 +767,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -750,6 +809,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -768,6 +828,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -809,6 +870,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -827,6 +889,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -868,6 +931,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -909,6 +973,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -927,6 +992,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -968,6 +1034,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -986,6 +1053,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -1033,6 +1101,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -1057,6 +1126,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -1104,6 +1174,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -1128,6 +1199,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -1183,6 +1255,7 @@ mod tests { ranked_countable, ranked_summable, ranked_averageable, + time_range: None, } } @@ -1339,6 +1412,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let mut new_indices = old_indices.clone(); diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs index fd773b07c5f..9a10bfaa296 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs @@ -95,6 +95,41 @@ pub trait DocumentTypeV0Methods: DocumentTypeV0Getters + DocumentTypeV0MethodsVe } } + /// [`Self::index_for_types`] restricted to the indexes `filter` admits. + /// + /// Candidates rejected by `filter` are skipped before they are scored, so + /// they can never be returned. This is the only correct way to require a + /// property of the selected index: because several indexes can cover the + /// same fields and ties are broken by the index map's name ordering, + /// checking the property after an unrestricted search can reject the + /// winner but cannot surface the index the caller actually needed. + /// + /// Shares the `index_for_types` feature-version gate — the filter narrows + /// the candidate set, it does not change how a candidate is scored. + fn index_for_types_matching( + &self, + index_names: &[&str], + in_field_name: Option<&str>, + order_by: &[&str], + filter: impl Fn(&Index) -> bool, + platform_version: &PlatformVersion, + ) -> Result, ProtocolError> { + match platform_version + .dpp + .contract_versions + .document_type_versions + .methods + .index_for_types + { + 0 => Ok(self.index_for_types_matching_v0(index_names, in_field_name, order_by, filter)), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "index_for_types_matching".to_string(), + known_versions: vec![0], + received: version, + }), + } + } + fn serialize_value_for_key( &self, key: &str, diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs index 1501184adfb..0c882423d35 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs @@ -403,10 +403,32 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa index_names: &[&str], in_field_name: Option<&str>, order_by: &[&str], + ) -> Option<(&Index, u16)> { + self.index_for_types_matching_v0(index_names, in_field_name, order_by, |_| true) + } + + /// [`Self::index_for_types_v0`] restricted to the indexes `filter` admits. + /// + /// The filter is applied before `Index::matches`, so a rejected index can + /// never win the difference comparison. Callers that need selection pinned + /// to a class of index must use this rather than post-checking whatever the + /// unrestricted search returned: several indexes can cover the same fields, + /// the winner among equally-good candidates is decided by the index map's + /// name ordering, and a post-check can only reject the winner — never + /// promote the index that was actually required. + fn index_for_types_matching_v0( + &self, + index_names: &[&str], + in_field_name: Option<&str>, + order_by: &[&str], + filter: impl Fn(&Index) -> bool, ) -> Option<(&Index, u16)> { let mut best_index: Option<(&Index, u16)> = None; let mut best_difference = u16::MAX; for (_, index) in self.indexes().iter() { + if !filter(index) { + continue; + } let difference_option = index.matches(index_names, in_field_name, order_by); if let Some(difference) = difference_option { if difference == 0 { 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 f1522e5f888..c3ab4962f40 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 @@ -3,7 +3,7 @@ use crate::data_contract::accessors::v1::DataContractV1Getters; use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; -use crate::data_contract::document_type::Index; +use crate::data_contract::document_type::{Index, IndexGrammarAdmissions}; use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::fee::Credits; use crate::prelude::DataContract; @@ -112,23 +112,26 @@ impl DataContractInSerializationFormat { ) { for index_value in index_values { if let Ok(index_value_map) = index_value.to_map() { - // 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 + // Same keyword gates the document type parser + // applies, read from the one shared generation → + // admission mapping. Without them a PV14 index + // carrying `rankedCountable` &co. or `timeRange` + // 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(), + let admissions = IndexGrammarAdmissions::for_schema_generation( platform_version .dpp .contract_versions .document_type_versions .schema - .document_type_schema - >= 3, + .document_type_schema, + ); + if let Ok(index) = Index::try_from_value_map( + index_value_map.as_slice(), + admissions.ranked, + admissions.time_range, ) { let base_index_fee = if index.contested_index.is_some() { fee_version.document_type_base_contested_index_registration_fee diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 6d8a854e32d..f3e3fccbc69 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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } nonempty = "0.11" # Shielded-pool snapshot needs raw RocksDB SstFileWriter + ingest_external_file_cf # bindings, and blake3 for the snapshot-file checksum. @@ -90,6 +90,7 @@ rocksdb = { git = "https://github.com/QuantumExplorer/rust-rocksdb.git", rev = " blake3 = "1.5" [dev-dependencies] +dash-platform-queries = { path = "../dash-platform-queries" } platform-version = { path = "../rs-platform-version", features = [ "mock-versions", ] } @@ -107,7 +108,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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", features = ["client"] } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", 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 +122,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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } -grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } +grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } +grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } [features] default = ["bls-signatures"] diff --git a/packages/rs-drive-abci/src/abci/handler/prepare_proposal.rs b/packages/rs-drive-abci/src/abci/handler/prepare_proposal.rs index 10a4bd95631..5a0361b66ac 100644 --- a/packages/rs-drive-abci/src/abci/handler/prepare_proposal.rs +++ b/packages/rs-drive-abci/src/abci/handler/prepare_proposal.rs @@ -13,7 +13,7 @@ use crate::rpc::core::CoreRPCLike; use dpp::dashcore::hashes::Hash; use dpp::dashcore::Network; use dpp::version::TryIntoPlatformVersioned; -use drive::grovedb_storage::Error::RocksDBError; +use drive::grovedb::error::Error as GroveDBError; use tenderdash_abci::proto::abci as proto; use tenderdash_abci::proto::abci::tx_record::TxAction; use tenderdash_abci::proto::abci::{ExecTxResult, TxRecord}; @@ -142,7 +142,7 @@ where ); if let Some(tx) = transaction_guard.as_ref() { tx.rollback_to_savepoint() - .map_err(|e| drive::grovedb::error::Error::StorageError(RocksDBError(e)))?; + .map_err(GroveDBError::StorageError)?; tx.set_savepoint(); } transaction_guard diff --git a/packages/rs-drive-abci/src/abci/handler/process_proposal.rs b/packages/rs-drive-abci/src/abci/handler/process_proposal.rs index a64aba5013b..db05c55fb77 100644 --- a/packages/rs-drive-abci/src/abci/handler/process_proposal.rs +++ b/packages/rs-drive-abci/src/abci/handler/process_proposal.rs @@ -14,7 +14,7 @@ use crate::platform_types::state_transitions_processing_result::StateTransitionE use crate::rpc::core::CoreRPCLike; use dpp::dashcore::Network; use dpp::version::TryIntoPlatformVersioned; -use drive::grovedb_storage::Error::RocksDBError; +use drive::grovedb::error::Error as GroveDBError; use tenderdash_abci::proto::abci as proto; use tenderdash_abci::proto::abci::tx_record::TxAction; @@ -173,7 +173,7 @@ where ); if let Some(tx) = transaction_guard.as_ref() { tx.rollback_to_savepoint() - .map_err(|e| drive::grovedb::error::Error::StorageError(RocksDBError(e)))?; + .map_err(GroveDBError::StorageError)?; tx.set_savepoint(); } transaction_guard diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs index bd37b545712..ba062b7f737 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs @@ -64,6 +64,7 @@ impl Platform { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index 4e19577b925..2a17f38dba9 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -17,7 +17,6 @@ use crate::platform_types::state_transitions_processing_result::{ use dpp::util::hash::hash_single; use dpp::version::PlatformVersion; use drive::grovedb::Transaction; -use drive::grovedb_storage::Error::RocksDBError; use std::time::Instant; use super::super::StateTransitionAwareError; @@ -224,7 +223,7 @@ where // failure means the proposal can no longer match the // block — fail it rather than continue on leaked state. transaction.rollback_to_savepoint().map_err(|e| { - drive::grovedb::error::Error::StorageError(RocksDBError(e)) + drive::grovedb::error::Error::StorageError(e) })?; } StateTransitionExecutionResult::SuccessfulExecution { .. } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 384aa8781fe..ffe9cbe7b20 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -248,6 +248,7 @@ pub(super) fn create_domain_data_trigger_v0( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation @@ -339,6 +340,7 @@ pub(super) fn create_domain_data_trigger_v0( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs index 06fec8661c2..17ee3e1b21c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs @@ -241,6 +241,7 @@ pub(super) fn create_domain_data_trigger_v1( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Diff vs `_v0` (parent-domain query): @@ -354,6 +355,7 @@ pub(super) fn create_domain_data_trigger_v1( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Diff vs `_v0` (preorder query): same change as above. `_v0` diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index ccf7a81837f..0f399d63ff9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -77,6 +77,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs index 66c14377bdc..13d59b238f6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs @@ -71,6 +71,7 @@ pub(super) fn delete_withdrawal_data_trigger_v1( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Diff vs `_v0` (withdrawal-document lookup): diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 8a5b5a7d41b..b8f3b475887 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -122,6 +122,7 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v0( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation @@ -182,6 +183,7 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Diff vs `_v0`: epoch is `Some(...)` and the cost is billed via @@ -307,6 +309,7 @@ fn fetch_document_with_id_v0( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation @@ -369,6 +372,7 @@ fn fetch_document_with_id_v1( start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Diff vs `_v0`: epoch is `Some(...)` and the cost is billed via diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs index 7a94babf225..f6528d6afad 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs @@ -456,6 +456,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let documents = platform @@ -503,6 +504,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let documents = platform @@ -911,6 +913,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let documents = platform @@ -945,6 +948,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let documents = platform @@ -1178,6 +1182,7 @@ mod dpns_username_transfer_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; platform @@ -1273,6 +1278,7 @@ mod dpns_username_transfer_tests { limit: None, prove: false, drive_config: &platform.config.drive, + resolved_time_ranges: vec![], }; match platform @@ -1316,6 +1322,7 @@ mod dpns_username_transfer_tests { limit: None, prove: false, drive_config: &platform.config.drive, + resolved_time_ranges: vec![], }; match platform @@ -1359,6 +1366,7 @@ mod dpns_username_transfer_tests { limit: None, prove: false, drive_config: &platform.config.drive, + resolved_time_ranges: vec![], }; match platform @@ -1426,6 +1434,7 @@ mod dpns_username_transfer_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; platform diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/ranked_group_drain.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/ranked_group_drain.rs index 10ada3042f7..8e97e3826b1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/ranked_group_drain.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/ranked_group_drain.rs @@ -39,7 +39,10 @@ use dpp::state_transition::batch_transition::document_delete_transition::Documen use dpp::state_transition::batch_transition::BatchTransitionV1; use dpp::state_transition::StateTransition; use drive::drive::RootTree; -use drive::grovedb::Element; +use drive::grovedb::element::IndexAxis; +use drive::grovedb::operations::proof::indexed_axis::AxisEntries; +use drive::grovedb::query_result_type::QueryResultType; +use drive::grovedb::{Element, PathQuery, PathQueryRun}; /// The group that gets emptied. const G: &str = "beta"; @@ -75,19 +78,29 @@ fn ranked_count_groups( path: &[Vec], platform_version: &PlatformVersion, ) -> Vec<(u64, String)> { - let path_refs: Vec<&[u8]> = path.iter().map(|v| v.as_slice()).collect(); - platform + let path_query = PathQuery::new_axis_top_k(path.to_vec(), IndexAxis::Count, 100, 0, true); + let run = platform .drive .grove - .indexed_count_top_k( - path_refs.as_slice(), - 100, + .run_path_query( + &path_query, + true, + true, true, + QueryResultType::QueryPathKeyElementTrioResultType, None, &platform_version.drive.grove_version, ) .unwrap() - .expect("the ranked count read must succeed") + .expect("the ranked count read must succeed"); + let PathQueryRun::AxisEntries { + entries: AxisEntries::Count(entries), + .. + } = run + else { + panic!("expected count axis entries"); + }; + entries .into_iter() .map(|entry| entry.key_pair()) .map(|(count, key)| { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index 91344794871..179da4d0162 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -2600,6 +2600,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; query.internal_clauses.equal_clauses.insert( "contractId".to_string(), @@ -2982,6 +2983,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; query.internal_clauses.equal_clauses.insert( "contractId".to_string(), diff --git a/packages/rs-drive-abci/src/query/document_query/v0/mod.rs b/packages/rs-drive-abci/src/query/document_query/v0/mod.rs index ff5a9ab1686..bb245961d83 100644 --- a/packages/rs-drive-abci/src/query/document_query/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v0/mod.rs @@ -16,6 +16,7 @@ use dpp::platform_value::Value; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; use drive::error::query::QuerySyntaxError; +use drive::query::ResolvedTimeRange; use drive::query::{DriveDocumentQuery, OrderClause, WhereClause}; use drive::util::grove_operations::GroveDBToUse; @@ -125,6 +126,9 @@ impl Platform { data_contract_id, document_type_name, where_clauses, + // The v0 wire has no time-range operator, so nothing on this path + // can carry a resolved bucket equality. + Vec::new(), order_by_clauses, // v0 wire's `uint32` limit: `0` is the sentinel for // "use server default"; `> u16::MAX` is rejected. @@ -157,6 +161,7 @@ impl Platform { data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: Vec, order_by_clauses: Vec, limit_u32: Option, prove: bool, @@ -233,7 +238,7 @@ impl Platform { Some(n) => Some(n as u16), }; - let drive_query = + let mut drive_query = check_validation_result_with_data!(DriveDocumentQuery::from_typed_clauses( where_clauses, order_by_clauses, @@ -246,6 +251,11 @@ impl Platform { &self.config.drive, platform_version, )); + // Clause parsing cannot tell a resolved bucket equality from a + // hand-written one, so the provenance the v1 handler established is + // attached here; index selection reads it to pin the query to the + // index that buckets the field. + drive_query.resolved_time_ranges = resolved_time_ranges; let response = if prove { let proof = @@ -644,6 +654,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let request = GetDocumentsRequestV0 { @@ -717,6 +728,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let request = GetDocumentsRequestV0 { @@ -802,6 +814,7 @@ mod tests { start_at: Some(after), start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let request = GetDocumentsRequestV0 { @@ -974,6 +987,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let where_clauses = serialize_vec_to_cbor( @@ -1141,6 +1155,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let where_clauses = serialize_vec_to_cbor( @@ -1296,6 +1311,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let mut where_clauses: Vec<_> = drive_document_query @@ -1462,6 +1478,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let mut where_clauses: Vec<_> = drive_document_query @@ -1645,6 +1662,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let mut where_clauses: Vec<_> = drive_document_query 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 fa5c4da140b..b608a7c9952 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 @@ -36,9 +36,10 @@ use dapi_grpc::platform::v0::get_documents_request::{ WhereOperator as ProtoWhereOperator, }; use dpp::platform_value::Value; +use drive::query::TimeRangeGridSpec; use drive::query::{ HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, - OrderClause, SelectFunction, SelectProjection, WhereClause, WhereOperator, + OrderClause, SelectFunction, SelectProjection, TimeRangeSelector, WhereClause, WhereOperator, }; /// Map a wire-level [`ProtoWhereOperator`] discriminant onto @@ -49,7 +50,7 @@ use drive::query::{ pub(super) fn where_operator_from_proto(op: i32) -> Result { let proto_op = ProtoWhereOperator::try_from(op).map_err(|_| { QueryError::InvalidArgument(format!( - "unknown WhereOperator discriminant: {} (valid values: 0..=10, see \ + "unknown WhereOperator discriminant: {} (valid values: 0..=11, see \ `get_documents_request::WhereOperator`)", op )) @@ -66,9 +67,127 @@ pub(super) fn where_operator_from_proto(op: i32) -> Result WhereOperator::BetweenExcludeRight, ProtoWhereOperator::In => WhereOperator::In, ProtoWhereOperator::StartsWith => WhereOperator::StartsWith, + // IN_TIME_RANGE is not an engine operator: it's resolved to a concrete + // equality from authoritative block time before clause conversion (see + // `is_time_range_clause` / `time_range_clause_from_proto`), so it must + // never reach this mapping. + ProtoWhereOperator::InTimeRange => { + return Err(QueryError::InvalidArgument( + "IN_TIME_RANGE where clauses are resolved from block time before \ + operator conversion and must not be mixed into normal clause decoding" + .to_string(), + )) + } }) } +/// Whether a wire where clause is a time-range selection +/// (`operator == IN_TIME_RANGE`). The v1 handler partitions these out and +/// resolves them from authoritative block time via +/// [`time_range_clause_from_proto`]. +pub(super) fn is_time_range_clause(clause: &ProtoWhereClause) -> bool { + clause.operator == ProtoWhereOperator::InTimeRange as i32 +} + +/// Decode an `IN_TIME_RANGE` wire where clause into its +/// `(field, selector, grid)`. Two operand shapes: +/// +/// - `DocumentFieldValue.text` — the bare selector (`"newest"` / +/// `"oldest"`). Unambiguous only while exactly one grid buckets the +/// field; the resolver rejects it otherwise. +/// - `DocumentFieldValue.list` — `[text(selector), uint64(range), +/// uint64(step)]` or `[…, uint64(phase)]`, naming one grid in the +/// contract's own declared units (seconds). Required when several grids +/// bucket the field. Like the contract grammar and the storage key, a +/// zero phase is spelled by omission — the three-element form — so every +/// grid has exactly one wire spelling. +pub(super) fn time_range_clause_from_proto( + clause: ProtoWhereClause, +) -> Result<(String, TimeRangeSelector, Option), QueryError> { + let field = clause.field; + let parse_selector = |text: &str| { + TimeRangeSelector::from_string(text).ok_or_else(|| { + QueryError::InvalidArgument(format!( + "IN_TIME_RANGE selector must be \"newest\" or \"oldest\", got \"{}\"", + text + )) + }) + }; + match clause.value.and_then(|v| v.variant) { + Some(document_field_value::Variant::Text(selector_text)) => { + Ok((field, parse_selector(&selector_text)?, None)) + } + Some(document_field_value::Variant::List(list)) => { + let mut values = list.values.into_iter(); + let selector = match values.next().and_then(|v| v.variant) { + Some(document_field_value::Variant::Text(s)) => parse_selector(&s)?, + _ => { + return Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE list operand on field '{}' must start with the \ + text selector \"newest\" or \"oldest\"", + field + ))) + } + }; + let mut grid_number = |name: &str| -> Result { + match values.next().and_then(|v| v.variant) { + Some(document_field_value::Variant::Uint64Value(n)) => Ok(n), + _ => Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE list operand on field '{}' must carry the grid's \ + {} as a uint64 of seconds, exactly as the contract declares it", + field, name + ))), + } + }; + let range_seconds = grid_number("range")?; + let step_seconds = grid_number("step")?; + let phase_seconds = match values.next() { + None => 0, + Some(v) => match v.variant { + Some(document_field_value::Variant::Uint64Value(0)) => { + return Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE list operand on field '{}': a zero phase is \ + spelled by omission (use the three-element form), so every \ + grid has exactly one wire spelling", + field + ))) + } + Some(document_field_value::Variant::Uint64Value(n)) => n, + _ => { + return Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE list operand on field '{}' must carry the \ + grid's phase as a uint64 of seconds", + field + ))) + } + }, + }; + if values.next().is_some() { + return Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE list operand on field '{}' takes at most \ + [selector, range, step, phase]", + field + ))); + } + Ok(( + field, + selector, + Some(TimeRangeGridSpec { + range_seconds, + step_seconds, + phase_seconds, + }), + )) + } + _ => Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE clause on field '{}' must carry either a text operand \ + (\"newest\" / \"oldest\") or a list operand [selector, range, step] / \ + [selector, range, step, phase] naming one of the field's grids", + field + ))), + } +} + /// Map a wire [`ProtoDocumentFieldValue`] onto a /// `dpp::platform_value::Value`. Schema-agnostic — variants map /// 1:1 by primitive type and recurse for `list` up to a depth of diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/average.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/average.rs index 33a53487d65..f868ac9bc8a 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/average.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/average.rs @@ -1,6 +1,7 @@ //! `RoutingDecision::Average` — the grouped average surface. use super::super::not_yet_implemented; +use super::super::PrefetchedContract; use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; @@ -17,10 +18,10 @@ use dapi_grpc::platform::v0::get_documents_response::{ }; use dpp::check_validation_result_with_data; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; use drive::error::query::QuerySyntaxError; +use drive::query::ResolvedTimeRange; use drive::query::{ AverageEntry as DriveAverageEntry, AverageMode, CountMode, DocumentAverageRequest, DocumentAverageResponse, OrderClause, WhereClause, @@ -43,9 +44,11 @@ impl Platform { #[allow(clippy::too_many_arguments)] pub(in crate::query::document_query::v1) fn dispatch_average_v1( &self, + prefetched_contract: PrefetchedContract, data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: Vec, order_clauses: Vec, limit: Option, start: Option, @@ -62,25 +65,15 @@ impl Platform { ))); } - 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", - )) - )); + // The request's single contract fetch: reuse the time-range + // resolution's when it ran, fetch now otherwise — after the cheap + // shape guards above, so their rejections keep precedence. + let (contract_id, contract_fetch_info) = check_validation_result_with_data!(self + .contract_for_aggregate_dispatch( + prefetched_contract, + data_contract_id, + platform_version + )?); 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()) @@ -102,6 +95,7 @@ impl Platform { document_type, sum_property, where_clauses, + resolved_time_ranges, order_clauses, mode: avg_mode, limit, diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/count.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/count.rs index 7af8f56ef41..5f4f3c921dd 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/count.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/count.rs @@ -2,6 +2,7 @@ //! per-group entries). use super::super::not_yet_implemented; +use super::super::PrefetchedContract; use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; @@ -17,10 +18,10 @@ use dapi_grpc::platform::v0::get_documents_response::{ }; use dpp::check_validation_result_with_data; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; use drive::error::query::QuerySyntaxError; +use drive::query::ResolvedTimeRange; use drive::query::{ CountMode, DocumentCountRequest, DocumentCountResponse, OrderClause, SplitCountEntry, WhereClause, @@ -39,9 +40,11 @@ impl Platform { #[allow(clippy::too_many_arguments)] pub(in crate::query::document_query::v1) fn dispatch_count_v1( &self, + prefetched_contract: PrefetchedContract, data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: Vec, order_clauses: Vec, limit: Option, start: Option, @@ -57,25 +60,15 @@ impl Platform { ))); } - 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", - )) - )); + // The request's single contract fetch: reuse the time-range + // resolution's when it ran, fetch now otherwise — after the cheap + // shape guards above, so their rejections keep precedence. + let (contract_id, contract_fetch_info) = check_validation_result_with_data!(self + .contract_for_aggregate_dispatch( + prefetched_contract, + data_contract_id, + platform_version + )?); 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()) @@ -88,6 +81,7 @@ impl Platform { contract: contract_ref, document_type, where_clauses, + resolved_time_ranges, order_clauses, mode, limit, diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/documents.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/documents.rs index 9dd9419363c..c2cefc2ba3b 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/documents.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/documents.rs @@ -13,6 +13,7 @@ use dapi_grpc::platform::v0::get_documents_response::{ get_documents_response_v0, get_documents_response_v1, GetDocumentsResponseV1, }; use dpp::version::PlatformVersion; +use drive::query::ResolvedTimeRange; use drive::query::{OrderClause, WhereClause}; impl Platform { @@ -28,6 +29,7 @@ impl Platform { data_contract_id: Vec, document_type: String, where_clauses: Vec, + resolved_time_ranges: Vec, order_by_clauses: Vec, limit: Option, start: Option, @@ -48,6 +50,7 @@ impl Platform { data_contract_id, document_type, where_clauses, + resolved_time_ranges, order_by_clauses, limit, prove, diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/having.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/having.rs index eb561a62775..cc874ab9c40 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/having.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/having.rs @@ -1,6 +1,7 @@ //! `RoutingDecision::HavingRange` — the boolean-HAVING range //! surface (PV14). +use super::super::PrefetchedContract; use super::{empty_ranking_proof_rejection, into_v1_ranked_entry}; use crate::error::query::QueryError; use crate::error::Error; @@ -17,10 +18,9 @@ use dapi_grpc::platform::v0::get_documents_response::{ }; use dpp::check_validation_result_with_data; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::error::query::QuerySyntaxError; +use drive::query::ResolvedTimeRange; use drive::query::{ DocumentHavingRequest, DocumentHavingResponse, HavingClause, OrderClause, SelectProjection, WhereClause, @@ -45,12 +45,14 @@ impl Platform { #[allow(clippy::too_many_arguments)] pub(in crate::query::document_query::v1) fn dispatch_having_v1( &self, + prefetched_contract: PrefetchedContract, data_contract_id: Vec, document_type_name: String, select: SelectProjection, group_by: Vec, having: Vec, where_clauses: Vec, + resolved_time_ranges: Vec, order_clauses: Vec, limit: Option, offset: Option, @@ -59,25 +61,15 @@ impl Platform { 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", - )) - )); + // The request's single contract fetch: reuse the time-range + // resolution's when it ran, fetch now otherwise — after the cheap + // shape guards above, so their rejections keep precedence. + let (contract_id, contract_fetch_info) = check_validation_result_with_data!(self + .contract_for_aggregate_dispatch( + prefetched_contract, + data_contract_id, + platform_version + )?); 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()) @@ -94,6 +86,7 @@ impl Platform { having: &having, order_by: &order_clauses, where_clauses: &where_clauses, + resolved_time_ranges: &resolved_time_ranges, limit, offset, has_start_at: start.is_some(), diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/mod.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/mod.rs index 4c2d0315f48..df6b78461cf 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/mod.rs @@ -58,10 +58,11 @@ fn into_v1_ranked_entry(e: DriveRankedEntry) -> RankedEntry { /// 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 +/// prover moved to the paginated axis traversal (today through +/// grovedb's unified `prove_query`), 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 diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/ranked.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/ranked.rs index 2d75bc4b4f1..7c990672a96 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/ranked.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/ranked.rs @@ -1,5 +1,6 @@ //! `RoutingDecision::Ranked` — the ranked top-k surface (PV14). +use super::super::PrefetchedContract; use super::{empty_ranking_proof_rejection, into_v1_ranked_entry}; use crate::error::query::QueryError; use crate::error::Error; @@ -16,10 +17,9 @@ use dapi_grpc::platform::v0::get_documents_response::{ }; use dpp::check_validation_result_with_data; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::error::query::QuerySyntaxError; +use drive::query::ResolvedTimeRange; use drive::query::{ DocumentRankedRequest, DocumentRankedResponse, HavingClause, OrderClause, SelectProjection, WhereClause, @@ -54,12 +54,14 @@ impl Platform { #[allow(clippy::too_many_arguments)] pub(in crate::query::document_query::v1) fn dispatch_ranked_v1( &self, + prefetched_contract: PrefetchedContract, data_contract_id: Vec, document_type_name: String, select: SelectProjection, group_by: Vec, having: Vec, where_clauses: Vec, + resolved_time_ranges: Vec, order_clauses: Vec, limit: Option, offset: Option, @@ -68,25 +70,15 @@ impl Platform { 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", - )) - )); + // The request's single contract fetch: reuse the time-range + // resolution's when it ran, fetch now otherwise — after the cheap + // shape guards above, so their rejections keep precedence. + let (contract_id, contract_fetch_info) = check_validation_result_with_data!(self + .contract_for_aggregate_dispatch( + prefetched_contract, + data_contract_id, + platform_version + )?); 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()) @@ -103,6 +95,7 @@ impl Platform { having: &having, order_by: &order_clauses, where_clauses: &where_clauses, + resolved_time_ranges: &resolved_time_ranges, limit, offset, has_start_at: start.is_some(), diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/sum.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/sum.rs index 5a495cef98d..b5fc7a064d1 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/sum.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/sum.rs @@ -1,6 +1,7 @@ //! `RoutingDecision::Sum` — the grouped sum surface. use super::super::not_yet_implemented; +use super::super::PrefetchedContract; use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; @@ -16,10 +17,10 @@ use dapi_grpc::platform::v0::get_documents_response::{ }; use dpp::check_validation_result_with_data; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; use drive::error::query::QuerySyntaxError; +use drive::query::ResolvedTimeRange; use drive::query::{ CountMode, DocumentSumRequest, DocumentSumResponse, OrderClause, SumEntry as DriveSumEntry, SumMode, WhereClause, @@ -41,9 +42,11 @@ impl Platform { #[allow(clippy::too_many_arguments)] pub(in crate::query::document_query::v1) fn dispatch_sum_v1( &self, + prefetched_contract: PrefetchedContract, data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: Vec, order_clauses: Vec, limit: Option, start: Option, @@ -60,25 +63,15 @@ impl Platform { ))); } - 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", - )) - )); + // The request's single contract fetch: reuse the time-range + // resolution's when it ran, fetch now otherwise — after the cheap + // shape guards above, so their rejections keep precedence. + let (contract_id, contract_fetch_info) = check_validation_result_with_data!(self + .contract_for_aggregate_dispatch( + prefetched_contract, + data_contract_id, + platform_version + )?); 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()) @@ -102,6 +95,7 @@ impl Platform { document_type, sum_property, where_clauses, + resolved_time_ranges, order_clauses, mode: sum_mode, limit, 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 4a7fe596e1c..58c227bff34 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 @@ -31,6 +31,7 @@ mod conversions; mod dispatch; mod routing; +use drive::query::ResolvedTimeRange; use routing::{reject_offset_off_the_ranked_path, validate_and_route}; // Re-exported so `tests.rs` (a `use super::*` consumer) keeps seeing // the routing probe under its old name. @@ -55,12 +56,19 @@ use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; +use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_documents_request::GetDocumentsRequestV1; use dapi_grpc::platform::v0::get_documents_response::GetDocumentsResponseV1; +use dpp::check_validation_result_with_data; +use dpp::data_contract::accessors::v0::DataContractV0Getters as _; +use dpp::prelude::Identifier as ContractIdentifier; +use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; +use drive::drive::contract::DataContractFetchInfo; use drive::error::query::QuerySyntaxError; -use drive::query::{CountMode, SelectProjection}; +use drive::query::{resolve_time_range_bucket_clause, CountMode, SelectProjection}; +use std::sync::Arc; /// Build a `QuerySyntaxError::Unsupported` carrying a stable /// " is not yet implemented" message. The wording is @@ -75,6 +83,12 @@ pub(super) fn not_yet_implemented(feature: &str) -> QueryError { ))) } +/// The contract fetched once per request — by time-range resolution when +/// the request carries an `IN_TIME_RANGE` clause — and handed to the +/// aggregate dispatchers so they never fetch a second time. +pub(in crate::query::document_query::v1) type PrefetchedContract = + Option<(ContractIdentifier, Arc)>; + /// Outcome of `validate_and_route` — names the path the v1 request /// will dispatch to. /// @@ -137,6 +151,64 @@ enum RoutingDecision { } impl Platform { + /// Parse the wire contract id and fetch the contract (through drive's + /// cache) with the uniform error mapping every v1 document route uses. + /// One fetch per request: time-range resolution and the aggregate + /// dispatch arms both consume this instead of carrying their own + /// copies of the parse → fetch → not-found sequence. The document-type + /// lookup stays at each consumer — it borrows from the returned `Arc`. + /// + /// Outer `Err` is an internal error; inner `Err` is the validation + /// error to hand back to the wire caller + /// (`check_validation_result_with_data!` unwraps the nesting). + #[allow(clippy::type_complexity)] + fn fetch_contract_for_document_query_v1( + &self, + data_contract_id: Vec, + platform_version: &PlatformVersion, + ) -> Result), QueryError>, Error> { + let contract_id: ContractIdentifier = match data_contract_id.try_into() { + Ok(id) => id, + Err(_) => { + return Ok(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 Some(contract_fetch_info) = contract_fetch_info else { + return Ok(Err(QueryError::Query( + QuerySyntaxError::DataContractNotFound("contract not found for a document query"), + ))); + }; + Ok(Ok((contract_id, contract_fetch_info))) + } + + /// The contract an aggregate dispatch arm executes against: the + /// time-range resolution's fetch when it ran, one fetch now + /// otherwise. Called by each aggregate dispatcher AFTER its cheap + /// shape guards, so their rejections keep precedence over + /// contract-lookup errors. Same nested-`Result` contract as + /// [`Self::fetch_contract_for_document_query_v1`]. + #[allow(clippy::type_complexity)] + pub(in crate::query::document_query::v1) fn contract_for_aggregate_dispatch( + &self, + prefetched: PrefetchedContract, + data_contract_id: Vec, + platform_version: &PlatformVersion, + ) -> Result), QueryError>, Error> { + match prefetched { + Some(pair) => Ok(Ok(pair)), + None => self.fetch_contract_for_document_query_v1(data_contract_id, platform_version), + } + } + pub(super) fn query_documents_v1( &self, request_v1: GetDocumentsRequestV1, @@ -182,10 +254,98 @@ impl Platform { // `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) { + // + // Time-range (IN_TIME_RANGE) clauses are partitioned out first. They + // are resolved to concrete equality clauses on the bucketed source + // field using the authoritative committed block time, so the rest of + // the v1 pipeline (routing, executors, proofs) treats them as + // ordinary equality lookups. The verifier re-derives the same bucket + // from the quorum-signed response metadata time, so the proof + // matches. + // The `.any()` pre-check skips the two-Vec partition on the + // overwhelmingly common clause set with no time-range operator. + let (time_range_proto, normal_proto): (Vec<_>, Vec<_>) = if proto_where_clauses + .iter() + .any(conversions::is_time_range_clause) + { + proto_where_clauses + .into_iter() + .partition(conversions::is_time_range_clause) + } else { + (Vec::new(), proto_where_clauses) + }; + + let mut where_clauses = match conversions::where_clauses_from_proto(normal_proto) { Ok(c) => c, Err(e) => return Ok(QueryValidationResult::new_with_error(e)), }; + let mut resolved_time_ranges: Vec = Vec::new(); + // The contract fetched for time-range resolution, handed to the + // aggregate dispatchers below so a request fetches at most once. + let mut prefetched_contract: PrefetchedContract = None; + + if !time_range_proto.is_empty() { + // LOAD-BEARING TIME SOURCE: the verifier re-derives the bucket + // from the response metadata's `time_ms`, which + // `response_metadata_v0` stamps from the state selected by + // `CheckpointUsed`. Reading `platform_state` here matches that + // only because every v1 document dispatch serves from + // `GroveDBToUse::Current`. If any document route ever serves + // from a checkpoint, this resolution must read the SAME state + // the response metadata will be built from, or every time-range + // proof will fail client-side verification. + let block_time_ms = + match platform_state.last_committed_block_time_ms() { + Some(t) => t, + None => return Ok(QueryValidationResult::new_with_error(QueryError::Query( + QuerySyntaxError::Unsupported( + "a time range (IN_TIME_RANGE) query requires a committed block time" + .to_string(), + ), + ))), + }; + let (contract_id, contract_fetch_info) = check_validation_result_with_data!(self + .fetch_contract_for_document_query_v1( + data_contract_id.clone(), + platform_version + )?); + let contract_ref = &contract_fetch_info.contract; + let doc_type = check_validation_result_with_data!(contract_ref + .document_type_for_name(document_type.as_str()) + .map_err(|_| QueryError::InvalidArgument(format!( + "document type {} not found for contract {}", + document_type, contract_id + )))); + for proto_wc in time_range_proto { + let (field, selector, grid) = + match conversions::time_range_clause_from_proto(proto_wc) { + Ok(parsed) => parsed, + Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + }; + match resolve_time_range_bucket_clause( + &field, + selector, + grid, + doc_type, + block_time_ms, + ) { + Ok((clause, resolved)) => { + where_clauses.push(clause); + // The resolved clause is an ordinary equality; only + // this list tells the executors that it must be + // matched against bucket starts of the resolved + // grid rather than raw timestamps (or another + // grid's starts), so it travels with the request. + resolved_time_ranges.push(resolved); + } + Err(drive::error::Error::Query(qe)) => { + return Ok(QueryValidationResult::new_with_error(QueryError::Query(qe))) + } + Err(e) => return Err(e.into()), + } + } + prefetched_contract = Some((contract_id, contract_fetch_info)); + } let order_by_clauses = match conversions::order_clauses_from_proto(proto_order_by) { Ok(c) => c, Err(e) => return Ok(QueryValidationResult::new_with_error(e)), @@ -238,10 +398,15 @@ impl Platform { } match routing { + // The documents route forwards the raw id into the shared + // `query_documents_typed` helper (v0 dispatches into it too), + // which owns its contract fetch; the aggregate arms below + // consume the request's single fetch instead. RoutingDecision::Documents => self.dispatch_documents_v1( data_contract_id, document_type, where_clauses, + resolved_time_ranges, order_by_clauses, limit, start, @@ -250,9 +415,11 @@ impl Platform { platform_version, ), RoutingDecision::Count(mode) => self.dispatch_count_v1( + prefetched_contract, data_contract_id, document_type, where_clauses, + resolved_time_ranges, order_by_clauses, limit, start, @@ -262,9 +429,11 @@ impl Platform { platform_version, ), RoutingDecision::Sum { sum_property, mode } => self.dispatch_sum_v1( + prefetched_contract, data_contract_id, document_type, where_clauses, + resolved_time_ranges, order_by_clauses, limit, start, @@ -275,9 +444,11 @@ impl Platform { platform_version, ), RoutingDecision::Average { sum_property, mode } => self.dispatch_average_v1( + prefetched_contract, data_contract_id, document_type, where_clauses, + resolved_time_ranges, order_by_clauses, limit, start, @@ -288,12 +459,14 @@ impl Platform { platform_version, ), RoutingDecision::Ranked => self.dispatch_ranked_v1( + prefetched_contract, data_contract_id, document_type, select, group_by, having_clauses, where_clauses, + resolved_time_ranges, order_by_clauses, limit, offset, @@ -303,12 +476,14 @@ impl Platform { platform_version, ), RoutingDecision::HavingRange => self.dispatch_having_v1( + prefetched_contract, data_contract_id, document_type, select, group_by, having_clauses, where_clauses, + resolved_time_ranges, order_by_clauses, limit, offset, diff --git a/packages/rs-drive-abci/src/query/document_query/v1/routing.rs b/packages/rs-drive-abci/src/query/document_query/v1/routing.rs index f686ffb47f4..91dd096e96c 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/routing.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/routing.rs @@ -368,7 +368,22 @@ pub(super) fn validate_and_route_for_tests( // `where_clauses` slice for the routing decision, because // the depth-cap and similar decode-time contracts aren't // exercisable otherwise. - conversions::where_clauses_from_proto(request_v1.where_clauses.clone())?; + // + // Time-range (IN_TIME_RANGE) clauses are partitioned out first — + // the same order `query_documents_v1` decodes in. The real handler + // resolves them into bucket equalities from committed block time; + // this stateless helper has no block time (or contract), so it + // only validates the clause shape here. Routing-relevant callers + // pass the resolved equality in their pre-decoded slice. + let (time_range_proto, normal_proto): (Vec<_>, Vec<_>) = request_v1 + .where_clauses + .clone() + .into_iter() + .partition(conversions::is_time_range_clause); + for proto_wc in time_range_proto { + conversions::time_range_clause_from_proto(proto_wc)?; + } + conversions::where_clauses_from_proto(normal_proto)?; // 2. ORDER BY decoding — aggregate-target reject as // `Unsupported("ORDER BY on aggregate keys …")`. let order_by_clauses = conversions::order_clauses_from_proto(request_v1.order_by.clone())?; 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 684e13db5e8..a1ddb2363ea 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 @@ -26,6 +26,7 @@ use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::random_document::CreateRandomDocument; use dpp::platform_value::{platform_value, Value}; use drive::query::WhereOperator; +use drive::query::{resolve_time_range_bucket_clause, validate_resolved_time_range_clause_shapes}; /// Build a `ProtoDocumentFieldValue` from a `dpp::platform_value::Value` /// for use inside this test module only. **Subset of the SDK's @@ -3860,8 +3861,14 @@ mod having_trust_boundary { const TIME_MS: u64 = 1_755_000_000_000; /// Provider that knows exactly one quorum key — the test one. - struct TestQuorumProvider { - pubkey: [u8; 48], + /// + /// This and the two signing helpers below are `pub(super)` so the + /// sibling [`super::time_range_proof_verification`] suite signs its + /// commits through the same canonical construction; a second copy of + /// the tenderdash harness could drift from this one and quietly stop + /// testing the binding. + pub(super) struct TestQuorumProvider { + pub(super) pubkey: [u8; 48], } impl ContextProvider for TestQuorumProvider { @@ -3899,7 +3906,7 @@ mod having_trust_boundary { } /// A deterministic, valid BLS scalar — no RNG dependency. - fn quorum_secret_key() -> SecretKey { + pub(super) fn quorum_secret_key() -> SecretKey { let mut bytes = [0u8; 32]; bytes[31] = 42; SecretKey::::from_be_bytes(&bytes) @@ -4035,6 +4042,7 @@ mod having_trust_boundary { having: &having, order_by: &[], where_clauses: &[], + resolved_time_ranges: &[], limit: Some(10), offset: None, has_start_at: false, @@ -4070,7 +4078,7 @@ mod having_trust_boundary { /// Sign a tenderdash precommit whose state id carries `app_hash` — /// the same canonical construction `verify_tenderdash_proof` /// rebuilds on the verify side. - fn signed_proof( + pub(super) fn signed_proof( grovedb_proof: Vec, app_hash: &[u8; 32], mtd: &ResponseMetadata, @@ -4245,3 +4253,1297 @@ mod having_trust_boundary { ); } } + +mod time_range_proof_verification { + //! The whole time-range reconstruction sequence, run end to end + //! against a populated Drive: the handler resolves `IN_TIME_RANGE` + //! from committed block time, proves the resulting bucket query, + //! and the client re-derives the *identical* bucket from the + //! quorum-signed response metadata `time_ms`, re-runs the + //! provenance guard, re-picks the bucketed index and verifies the + //! proof over the GroveDB path that selection produces. Every link + //! in that chain is load-bearing: a resolution reading anything + //! but the signed time, a picker admitting the plain index, or a + //! guard accepting a shape the resolver never built would each + //! turn a wrong answer into a *proven* wrong answer. + //! + //! The headline assertion is the overlapping-window one. With + //! `range = 6h, step = 2h` a document is written under three + //! bucket keys at once, so a count that walked buckets rather + //! than addressing exactly one would return three times the + //! truth — and would verify, because the proof would be an + //! honest proof of the wrong query. + //! + //! Split, same as [`super::having_trust_boundary`]: proof + //! end-to-end lives here because generating proofs needs drive's + //! `server` feature and a populated platform; the client-surface + //! half (resolution from metadata time, provenance bookkeeping, + //! ordering) is offline-tested in rs-sdk, which builds drive with + //! `verify` only. + + use super::having_trust_boundary::{quorum_secret_key, signed_proof, TestQuorumProvider}; + use super::*; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dapi_grpc::platform::v0::get_documents_response::Version as ResponseVersion; + use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; + use dpp::block::block_info::BlockInfo; + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + use dpp::data_contract::document_type::DocumentTypeRef; + use dpp::data_contract::DataContractFactory; + use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; + use dpp::platform_value::platform_value; + use dpp::prelude::DataContract; + use drive::drive::Drive; + use drive::query::{ + DriveDocumentCountQuery, DriveDocumentQuery, TimeRangeSelector, WhereClause, + }; + use drive_proof_verifier::types::Documents; + use drive_proof_verifier::{ + verify_point_lookup_count_proof, Error as ProofVerifierError, FromProof, SplitCountEntry, + }; + use std::collections::BTreeMap; + + const DOCUMENT_TYPE: &str = "post"; + const CREATED_AT: &str = "$createdAt"; + const BUCKETED_INDEX: &str = "trending"; + const MINUTE_MS: u64 = 60_000; + const HOUR_MS: u64 = 3_600_000; + + /// A bucket start on the contract's grid: `origin` is 0 and the step is + /// two hours, and this is an exact multiple of two hours. Every timestamp + /// below is expressed relative to it. + const NEWEST_BUCKET_START_MS: u64 = 1_755_000_000_000; + /// Committed block time, one hour into the newest bucket — so the newest + /// active range is `NEWEST_BUCKET_START_MS` and there is a full hour of + /// bucket in which documents can sit. + const BLOCK_TIME_MS: u64 = NEWEST_BUCKET_START_MS + HOUR_MS; + const BLOCK_HEIGHT: u64 = 100; + const BLOCK_CORE_HEIGHT: u32 = 42; + const QUORUM_HASH: [u8; 32] = [3u8; 32]; + + /// `($createdAt, hashtag)` for the four posts, chosen so the newest + /// bucket contains: two `#ibiza` posts that *also* live in the two older + /// overlapping buckets (the double-count regression), and one `#berlin` + /// post (so the second index property has to do work). The third + /// `#ibiza` post starts one hour before the newest bucket, so it belongs + /// to the three *older* buckets and to none of the newest — the negative + /// control that pins the query to a single bucket rather than to "recent + /// enough". + const POSTS: [(u64, &str); 4] = [ + (NEWEST_BUCKET_START_MS + 10 * MINUTE_MS, "ibiza"), + (NEWEST_BUCKET_START_MS + 30 * MINUTE_MS, "ibiza"), + (NEWEST_BUCKET_START_MS - HOUR_MS, "ibiza"), + (NEWEST_BUCKET_START_MS + 15 * MINUTE_MS, "berlin"), + ]; + + /// A `countable` bucketed index over `(timeRange($createdAt), hashtag)` + /// with a six-hour window sliding every two hours — overlap factor 3 — + /// next to a plain index covering the same two fields the other way + /// round. The plain one exists so index selection has something wrong to + /// pick: it covers the identical clause field set, and only the + /// resolution provenance keeps the query off it. + fn register_trending_contract( + platform: &Platform, + platform_version: &PlatformVersion, + ) -> DataContract { + let factory = DataContractFactory::new(platform_version.protocol_version) + .expect("expected a factory"); + let schemas = platform_value!({ + DOCUMENT_TYPE: { + "type": "object", + "properties": { + "hashtag": { "type": "string", "maxLength": 63, "position": 0 }, + }, + "indices": [ + { + "name": BUCKETED_INDEX, + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "countable": true, + "timeRange": { "on": "$createdAt", "range": 21_600u64, "step": 7_200u64 }, + }, + { + "name": "byHashtag", + "properties": [{ "hashtag": "asc" }, { "$createdAt": "asc" }], + "countable": true, + }, + ], + "required": ["$createdAt", "hashtag"], + "additionalProperties": false, + } + }); + let contract = factory + .create_with_value_config(Identifier::new([7u8; 32]), 0, schemas, None, None) + .expect("the trending contract is well-formed") + .data_contract_owned(); + store_data_contract(platform, &contract, platform_version); + contract + } + + fn insert_posts( + platform: &Platform, + contract: &DataContract, + platform_version: &PlatformVersion, + ) -> Vec { + let document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + POSTS + .iter() + .enumerate() + .map(|(i, (created_at_ms, hashtag))| { + let mut document: Document = document_type + .random_document(Some(7_000 + i as u64), platform_version) + .expect("random document"); + document.set_properties(BTreeMap::from([( + "hashtag".to_string(), + Value::Text(hashtag.to_string()), + )])); + document.set_created_at(Some(*created_at_ms)); + store_document( + platform, + contract, + document_type, + &document, + platform_version, + ); + document + }) + .collect() + } + + fn root_hash(drive: &Drive, platform_version: &PlatformVersion) -> [u8; 32] { + drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("root hash must be readable") + } + + /// A state whose last committed block carries [`BLOCK_TIME_MS`] — the + /// handler resolves `IN_TIME_RANGE` from it and stamps it into the + /// response metadata, which is what makes the client's re-derivation + /// land on the same bucket. + fn state_with_committed_block_time( + base: &PlatformState, + drive: &Drive, + platform_version: &PlatformVersion, + ) -> PlatformState { + let mut state = base.clone(); + state.set_last_committed_block_info(Some( + ExtendedBlockInfoV0 { + basic_info: BlockInfo { + time_ms: BLOCK_TIME_MS, + height: BLOCK_HEIGHT, + core_height: BLOCK_CORE_HEIGHT, + epoch: Default::default(), + }, + app_hash: root_hash(drive, platform_version), + quorum_hash: [0u8; 32], + block_id_hash: [0u8; 32], + proposer_pro_tx_hash: [0u8; 32], + signature: [0u8; 96], + round: 0, + } + .into(), + )); + state + } + + /// Contract registered, posts inserted, and a platform state whose + /// committed block time is [`BLOCK_TIME_MS`]. + fn setup_trending( + platform: &TempPlatform, + base_state: &PlatformState, + platform_version: &PlatformVersion, + ) -> (DataContract, Vec, PlatformState) { + assert!( + platform_version.protocol_version >= 14, + "time-range indexes require protocol version 14 or later" + ); + let contract = register_trending_contract(platform, platform_version); + let documents = insert_posts(platform, &contract, platform_version); + let state = state_with_committed_block_time(base_state, &platform.drive, platform_version); + (contract, documents, state) + } + + /// `IN_TIME_RANGE($createdAt, "newest") AND hashtag = ` — the + /// selector rides the wire as an operator, never as a resolved value, so + /// the bucket in the proof can only have come from the node's committed + /// block time. + fn trending_where_clauses(hashtag: &str) -> Vec { + vec![ + wc( + "hashtag", + ProtoWhereOperator::Equal, + Value::Text(hashtag.to_string()), + ), + wc( + CREATED_AT, + ProtoWhereOperator::InTimeRange, + Value::Text(TimeRangeSelector::Newest.as_str().to_string()), + ), + ] + } + + fn trending_request( + contract_id: Vec, + hashtag: &str, + selects: Vec, + ) -> GetDocumentsRequestV1 { + GetDocumentsRequestV1 { + data_contract_id: contract_id, + document_type: DOCUMENT_TYPE.to_string(), + where_clauses: trending_where_clauses(hashtag), + order_by: Vec::new(), + limit: None, + start: None, + prove: true, + selects, + group_by: Vec::new(), + having: Vec::new(), + offset: None, + } + } + + /// Run the request through the real v1 handler and re-sign the proof it + /// produced: the test platform never signs a commit, so the tenderdash + /// binding is supplied here over the *live* root hash and the handler's + /// own response metadata. Tampering with either afterwards is what the + /// negative case does. + fn prove_and_sign( + platform: &TempPlatform, + state: &PlatformState, + request: GetDocumentsRequestV1, + platform_version: &PlatformVersion, + ) -> (Proof, ResponseMetadata, TestQuorumProvider) { + 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(), "errors: {:?}", result.errors); + let response = result.data.expect("data"); + let proof = match response.result { + Some(get_documents_response_v1::Result::Proof(proof)) => proof, + other => panic!("expected a proof, got {:?}", other), + }; + let mtd = response + .metadata + .expect("the handler stamps response metadata"); + assert_eq!( + mtd.time_ms, BLOCK_TIME_MS, + "the metadata time the client resolves from must be the committed \ + block time the server resolved from" + ); + + let secret_key = quorum_secret_key(); + let signed = signed_proof( + proof.grovedb_proof, + &root_hash(&platform.drive, platform_version), + &mtd, + &secret_key, + QUORUM_HASH, + ); + let provider = TestQuorumProvider { + pubkey: secret_key.public_key().0.to_compressed(), + }; + (signed, mtd, provider) + } + + /// Re-sign the handler's proof over *altered* metadata with the fixture + /// quorum key. The signature then verifies, so a rejection can only come + /// from the proof path itself: the verifier resolves the selector from + /// the altered time into the NEXT bucket, reconstructs that bucket's + /// path query, and the GroveDB proof over the original bucket cannot + /// satisfy it. Without the re-signing, the negative tests could pass at + /// signature verification and never reach that property. + fn resign_over( + platform: &TempPlatform, + proof: &Proof, + mtd: &ResponseMetadata, + platform_version: &PlatformVersion, + ) -> Proof { + signed_proof( + proof.grovedb_proof.clone(), + &root_hash(&platform.drive, platform_version), + mtd, + &quorum_secret_key(), + QUORUM_HASH, + ) + } + + /// The bucket start the transform puts `time_ms` in, computed straight + /// off the contract's declared window rather than off the constants + /// above — so a fixture edit that moves the grid cannot leave the + /// expectation behind. + fn expected_newest_bucket(contract: &DataContract, time_ms: u64) -> u64 { + contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists") + .indexes() + .get(BUCKETED_INDEX) + .expect("the bucketed index survives contract registration") + .time_range + .as_ref() + .expect("the bucketed index carries its transform") + .newest_active_start(time_ms) + .expect("the block time is inside an active range") + } + + /// The client's reconstruction sequence, step for step, exactly as the + /// SDK's count helper performs it: resolve the selector from the signed + /// metadata time, record the provenance, re-run the shape guard, pick + /// the index that provenance admits, rebuild the prover's count query. + /// + /// Returns the resolved bucket start alongside the query so callers can + /// assert on what the metadata time resolved to. + fn client_count_query<'a>( + contract: &'a DataContract, + document_type: &'a DocumentTypeRef<'a>, + hashtag: &str, + time_ms: u64, + ) -> (DriveDocumentCountQuery<'a>, u64) { + let mut where_clauses = vec![WhereClause { + field: "hashtag".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(hashtag.to_string()), + }]; + let (clause, resolution) = resolve_time_range_bucket_clause( + CREATED_AT, + TimeRangeSelector::Newest, + None, + *document_type, + time_ms, + ) + .expect("the metadata time falls inside an active range"); + let bucket_start = clause + .value + .to_integer::() + .expect("a resolved bucket start is a millisecond timestamp"); + where_clauses.push(clause); + let resolutions = vec![resolution]; + + validate_resolved_time_range_clause_shapes(&where_clauses, &resolutions) + .expect("resolution produces exactly the one equality the guard admits"); + + let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( + document_type.indexes(), + &where_clauses, + &resolutions, + ) + .expect("the bucketed index covers the resolved clause set"); + assert_eq!( + index.name, BUCKETED_INDEX, + "resolution provenance must pin selection to the bucketed index, not \ + to the plain index covering the same fields" + ); + + let query = DriveDocumentCountQuery { + document_type: *document_type, + contract_id: contract.id().to_buffer(), + document_type_name: DOCUMENT_TYPE.to_string(), + index, + where_clauses, + }; + (query, bucket_start) + } + + fn total_of(entries: &[SplitCountEntry]) -> u64 { + entries.iter().map(|e| e.count.unwrap_or_default()).sum() + } + + /// The headline case. Two `#ibiza` posts sit in the newest bucket and, + /// because the window overlaps three deep, each is *stored* under three + /// bucket keys. The verified count must be 2 — one per document — which + /// is only true if both sides addressed exactly the one bucket the + /// signed block time names. + #[test] + fn a_count_over_an_overlapping_bucket_verifies_and_counts_each_document_once() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + + let document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + let transform = document_type + .indexes() + .get(BUCKETED_INDEX) + .expect("the bucketed index survives contract registration") + .time_range + .as_ref() + .expect("the bucketed index carries its transform"); + assert_eq!( + transform.overlap_factor(), + 3, + "the fixture's whole point is that windows overlap" + ); + assert_eq!( + transform.containing_buckets(POSTS[0].0).len(), + 3, + "a matching document must really be stored under three bucket keys" + ); + + let request = trending_request(contract.id().to_vec(), "ibiza", select_count_star()); + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let client_document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + let (query, bucket_start) = + client_count_query(&contract, &client_document_type, "ibiza", mtd.time_ms); + assert_eq!( + bucket_start, + expected_newest_bucket(&contract, mtd.time_ms), + "the resolved bucket must be the transform's newest active start \ + for the signed time" + ); + assert_eq!(bucket_start, NEWEST_BUCKET_START_MS); + + let entries = verify_point_lookup_count_proof(&query, &proof, &mtd, version, &provider) + .expect("a correctly signed count over the resolved bucket must verify"); + + assert_eq!( + total_of(&entries), + 2, + "the two #ibiza posts in the newest bucket count once each — a \ + document living in three overlapping buckets must not count three \ + times, and the #ibiza post one hour older belongs to the previous \ + buckets only" + ); + } + + /// Move the signed time forward by one full step and the client resolves + /// a *different* bucket. Verification must fail hard: either the proof + /// cannot be replayed over the other bucket's path, or the tenderdash + /// binding rejects the altered metadata. What must never happen is a + /// second, differently-scoped count coming back verified. + #[test] + fn a_tampered_metadata_time_cannot_verify_a_different_bucket() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + + let request = trending_request(contract.id().to_vec(), "ibiza", select_count_star()); + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let step_ms = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists") + .indexes() + .get(BUCKETED_INDEX) + .expect("the bucketed index survives contract registration") + .time_range + .as_ref() + .expect("the bucketed index carries its transform") + .step_ms(); + + let mut tampered = mtd.clone(); + tampered.time_ms += step_ms; + + let client_document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + let (_honest_query, honest_bucket) = + client_count_query(&contract, &client_document_type, "ibiza", mtd.time_ms); + let (tampered_query, tampered_bucket) = + client_count_query(&contract, &client_document_type, "ibiza", tampered.time_ms); + assert_eq!( + tampered_bucket, + honest_bucket + step_ms, + "one step of tampering must move the resolution one bucket — \ + otherwise this test proves nothing about where the bucket comes from" + ); + + let error = + verify_point_lookup_count_proof(&tampered_query, &proof, &tampered, version, &provider) + .expect_err("an altered signed time must not yield a verified count"); + assert!( + matches!( + error, + ProofVerifierError::InvalidSignature { .. } + | ProofVerifierError::GroveDBError { .. } + | ProofVerifierError::DriveError { .. } + ), + "the rejection must be the proof or the signature binding, got: {error:?}" + ); + } + + /// The non-aggregate route through the same fixture: the documents path + /// resolves the selector, carries the provenance onto the drive query + /// and verifies the returned rows. Same bucket scoping, different + /// primitive — so a regression that only reached the documents index + /// picker still turns a test red. + #[test] + fn a_documents_proof_over_the_newest_bucket_verifies_to_the_bucket_members() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, documents, state) = setup_trending(&platform, &base_state, version); + + let request = trending_request(contract.id().to_vec(), "ibiza", select_documents()); + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + let mut where_clauses = vec![WhereClause { + field: "hashtag".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("ibiza".to_string()), + }]; + let (resolved_clause, resolution) = resolve_time_range_bucket_clause( + CREATED_AT, + TimeRangeSelector::Newest, + None, + document_type, + mtd.time_ms, + ) + .expect("the metadata time falls inside an active range"); + where_clauses.push(resolved_clause); + let mut drive_query = DriveDocumentQuery::from_typed_clauses( + where_clauses, + Vec::new(), + None, + None, + true, + None, + &contract, + document_type, + &platform.config.drive, + version, + ) + .expect("the resolved clause set builds a drive query"); + drive_query.resolved_time_ranges = vec![resolution]; + + let response = GetDocumentsResponse { + version: Some(ResponseVersion::V1(GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Proof(proof)), + metadata: Some(mtd), + })), + }; + let (verified, _mtd, _proof) = + >::maybe_from_proof_with_metadata( + drive_query, + response, + Network::Testnet, + version, + &provider, + ) + .expect("a correctly signed documents proof must verify"); + + let mut verified_ids: Vec<_> = verified + .expect("the newest bucket is not empty") + .into_iter() + .filter_map(|(id, document)| document.map(|_| id)) + .collect(); + verified_ids.sort(); + let mut expected_ids = vec![documents[0].id(), documents[1].id()]; + expected_ids.sort(); + assert_eq!( + verified_ids, expected_ids, + "only the two #ibiza posts inside the newest bucket are proven \ + members — the older #ibiza post and the #berlin post are not" + ); + } + + // ----- the SDK `FromProof` entry points ------------------------------- + // + // The tests above re-run the client sequence step by step so a failure + // names the broken link. These run the same signed responses through the + // aggregate `FromProof` entry points the SDK actually + // exposes — resolution from signed metadata time, provenance, shape + // guard, index pick and proof verification all happen *inside* the call + // — so a regression in how that layer wires the steps together (not just + // in a step) turns a test red. + + use dash_platform_queries::documents::document_query::DocumentQuery as SdkDocumentQuery; + use drive::query::SelectProjection; + use drive_proof_verifier::{DocumentAverage, DocumentCount, DocumentSum}; + use std::sync::Arc; + + const SUMMABLE_INDEX: &str = "trendingLikes"; + + /// `likes` per post, aligned with [`POSTS`]: the two newest-bucket + /// `#ibiza` posts carry 10 and 30 (sum 40, average 20), the older + /// `#ibiza` post carries 100 and the `#berlin` post 7 — both excluded, + /// so an aggregate that leaked either is off by an unmistakable amount. + const LIKES: [u64; 4] = [10, 30, 100, 7]; + + /// The summable twin of [`register_trending_contract`]: the same + /// six-hour/two-hour bucketed `(timeRange($createdAt), hashtag)` index, + /// but countable *and* summable over a `likes` property — the CountSum + /// value trees that SUM reads and AVG derives `(count, sum)` from. The + /// plain `byHashtag` index mirrors both aggregate declarations so index + /// selection again has a wrong-but-covering candidate to reject. + fn register_engagement_contract( + platform: &Platform, + platform_version: &PlatformVersion, + ) -> DataContract { + let factory = DataContractFactory::new(platform_version.protocol_version) + .expect("expected a factory"); + let schemas = platform_value!({ + DOCUMENT_TYPE: { + "type": "object", + "properties": { + "hashtag": { "type": "string", "maxLength": 63, "position": 0 }, + "likes": { "type": "integer", "position": 1 }, + }, + "indices": [ + { + "name": SUMMABLE_INDEX, + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "countable": true, + "summable": "likes", + "timeRange": { "on": "$createdAt", "range": 21_600u64, "step": 7_200u64 }, + }, + { + "name": "byHashtag", + "properties": [{ "hashtag": "asc" }, { "$createdAt": "asc" }], + "countable": true, + "summable": "likes", + }, + ], + "required": ["$createdAt", "hashtag", "likes"], + "additionalProperties": false, + } + }); + let contract = factory + .create_with_value_config(Identifier::new([8u8; 32]), 0, schemas, None, None) + .expect("the engagement contract is well-formed") + .data_contract_owned(); + store_data_contract(platform, &contract, platform_version); + contract + } + + /// [`insert_posts`] with a `likes` value from [`LIKES`] on each post. + fn insert_engagement_posts( + platform: &Platform, + contract: &DataContract, + platform_version: &PlatformVersion, + ) { + let document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + for (i, ((created_at_ms, hashtag), likes)) in POSTS.iter().zip(LIKES).enumerate() { + let mut document: Document = document_type + .random_document(Some(8_000 + i as u64), platform_version) + .expect("random document"); + document.set_properties(BTreeMap::from([ + ("hashtag".to_string(), Value::Text(hashtag.to_string())), + ("likes".to_string(), Value::U64(likes)), + ])); + document.set_created_at(Some(*created_at_ms)); + store_document( + platform, + contract, + document_type, + &document, + platform_version, + ); + } + } + + /// Engagement contract registered, liked posts inserted, and a platform + /// state whose committed block time is [`BLOCK_TIME_MS`]. + fn setup_engagement( + platform: &TempPlatform, + base_state: &PlatformState, + platform_version: &PlatformVersion, + ) -> (DataContract, PlatformState) { + let contract = register_engagement_contract(platform, platform_version); + insert_engagement_posts(platform, &contract, platform_version); + let state = state_with_committed_block_time(base_state, &platform.drive, platform_version); + (contract, state) + } + + /// The query a real SDK caller would build: the selector still pending in + /// `time_range_clauses`, to be resolved by the entry point itself from + /// the signed metadata time. + fn sdk_query( + contract: &DataContract, + hashtag: &str, + select: SelectProjection, + ) -> SdkDocumentQuery { + SdkDocumentQuery::new(Arc::new(contract.clone()), DOCUMENT_TYPE) + .expect("the fixture has this document type") + .with_select(select) + .with_where(WhereClause { + field: "hashtag".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(hashtag.to_string()), + }) + .with_time_range(CREATED_AT, TimeRangeSelector::Newest) + } + + /// Wrap a handler proof and its metadata the way the wire does, so the + /// entry points consume exactly what a node returns. + fn signed_response(proof: Proof, mtd: &ResponseMetadata) -> GetDocumentsResponse { + GetDocumentsResponse { + version: Some(ResponseVersion::V1(GetDocumentsResponseV1 { + result: Some(get_documents_response_v1::Result::Proof(proof)), + metadata: Some(mtd.clone()), + })), + } + } + + fn select_field(function: v1_select::Function, field: &str) -> Vec { + vec![V1Select { + function: function as i32, + field: field.to_string(), + }] + } + + /// Nudge the signed time one full step so the entry point's own + /// resolution lands on a different bucket than the proof covers. + fn one_step_later( + contract: &DataContract, + index: &str, + mtd: &ResponseMetadata, + ) -> ResponseMetadata { + let step_ms = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists") + .indexes() + .get(index) + .expect("the bucketed index survives contract registration") + .time_range + .as_ref() + .expect("the bucketed index carries its transform") + .step_ms(); + let mut tampered = mtd.clone(); + tampered.time_ms += step_ms; + tampered + } + + /// The rejection must come from proof reconstruction (GroveDB/Drive) — + /// the caller re-signed the altered metadata, so `InvalidSignature` + /// would mean the deeper property (resolve-later-bucket → mismatched + /// proof path) was never exercised. + fn assert_proof_path_rejection(error: ProofVerifierError) { + assert!( + matches!( + error, + ProofVerifierError::GroveDBError { .. } | ProofVerifierError::DriveError { .. } + ), + "the rejection must come from the proof path, got: {error:?}" + ); + } + + #[test] + fn a_count_proof_verifies_through_the_sdk_from_proof_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + + let request = trending_request(contract.id().to_vec(), "ibiza", select_count_star()); + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let query = sdk_query(&contract, "ibiza", SelectProjection::count_star()); + let (count, verified_mtd, _proof) = + >::maybe_from_proof_with_metadata( + query, + signed_response(proof, &mtd), + Network::Testnet, + version, + &provider, + ) + .expect("a correctly signed count must verify through the entry point"); + + assert_eq!( + count.expect("the newest bucket is not empty"), + DocumentCount(2), + "the two #ibiza posts in the newest bucket count once each through \ + the entry point, exactly as through the step-by-step sequence" + ); + assert_eq!( + verified_mtd.time_ms, BLOCK_TIME_MS, + "the metadata handed back is the one the resolution consumed" + ); + } + + #[test] + fn a_tampered_metadata_time_is_rejected_at_the_count_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + + let request = trending_request(contract.id().to_vec(), "ibiza", select_count_star()); + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + let tampered = one_step_later(&contract, BUCKETED_INDEX, &mtd); + + // Re-sign the altered metadata so the signature is valid — + // verification must then resolve the selector one step later, + // reconstruct the NEXT bucket's path query, and reject the + // original bucket's GroveDB proof. (Verification reconstructs the + // proof path BEFORE checking the signature, so an unsigned + // alteration would hit the same rejection without proving the + // signature binds anything; the binding itself is pinned by the + // trust-boundary tests above.) + let resigned = resign_over(&platform, &proof, &tampered, version); + let query = sdk_query(&contract, "ibiza", SelectProjection::count_star()); + let error = >::maybe_from_proof_with_metadata( + query, + signed_response(resigned, &tampered), + Network::Testnet, + version, + &provider, + ) + .expect_err("a validly re-signed later time must not verify the stale bucket's proof"); + assert_proof_path_rejection(error); + } + + #[test] + fn a_sum_proof_verifies_through_the_sdk_from_proof_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_engagement(&platform, &base_state, version); + + let request = GetDocumentsRequestV1 { + selects: select_field(v1_select::Function::Sum, "likes"), + ..trending_request(contract.id().to_vec(), "ibiza", Vec::new()) + }; + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let query = sdk_query(&contract, "ibiza", SelectProjection::sum("likes")); + let (sum, _mtd, _proof) = + >::maybe_from_proof_with_metadata( + query, + signed_response(proof, &mtd), + Network::Testnet, + version, + &provider, + ) + .expect("a correctly signed sum must verify through the entry point"); + + assert_eq!( + sum.expect("the newest bucket is not empty"), + DocumentSum(40), + "10 + 30 likes on the two newest-bucket #ibiza posts — the older \ + #ibiza post's 100 and the #berlin post's 7 must not leak in, and \ + overlap fan-out must not multiply the total" + ); + } + + #[test] + fn a_tampered_metadata_time_is_rejected_at_the_sum_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_engagement(&platform, &base_state, version); + + let request = GetDocumentsRequestV1 { + selects: select_field(v1_select::Function::Sum, "likes"), + ..trending_request(contract.id().to_vec(), "ibiza", Vec::new()) + }; + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + let tampered = one_step_later(&contract, SUMMABLE_INDEX, &mtd); + let resigned = resign_over(&platform, &proof, &tampered, version); + + let query = sdk_query(&contract, "ibiza", SelectProjection::sum("likes")); + let error = >::maybe_from_proof_with_metadata( + query, + signed_response(resigned, &tampered), + Network::Testnet, + version, + &provider, + ) + .expect_err("a validly re-signed later time must not verify the stale bucket's proof"); + assert_proof_path_rejection(error); + } + + #[test] + fn an_average_proof_verifies_through_the_sdk_from_proof_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_engagement(&platform, &base_state, version); + + let request = GetDocumentsRequestV1 { + selects: select_field(v1_select::Function::Avg, "likes"), + ..trending_request(contract.id().to_vec(), "ibiza", Vec::new()) + }; + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let query = sdk_query(&contract, "ibiza", SelectProjection::avg("likes")); + let (average, _mtd, _proof) = + >::maybe_from_proof_with_metadata( + query, + signed_response(proof, &mtd), + Network::Testnet, + version, + &provider, + ) + .expect("a correctly signed average must verify through the entry point"); + + let average = average.expect("the newest bucket is not empty"); + assert_eq!( + average, + DocumentAverage { count: 2, sum: 40 }, + "the verified pair is the newest bucket's two #ibiza posts and \ + their 40 likes — nothing from outside the bucket, nothing doubled" + ); + assert_eq!(average.as_f64(), Some(20.0)); + } + + #[test] + fn a_tampered_metadata_time_is_rejected_at_the_average_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_engagement(&platform, &base_state, version); + + let request = GetDocumentsRequestV1 { + selects: select_field(v1_select::Function::Avg, "likes"), + ..trending_request(contract.id().to_vec(), "ibiza", Vec::new()) + }; + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + let tampered = one_step_later(&contract, SUMMABLE_INDEX, &mtd); + let resigned = resign_over(&platform, &proof, &tampered, version); + + let query = sdk_query(&contract, "ibiza", SelectProjection::avg("likes")); + let error = + >::maybe_from_proof_with_metadata( + query, + signed_response(resigned, &tampered), + Network::Testnet, + version, + &provider, + ) + .expect_err("a validly re-signed later time must not verify the stale bucket's proof"); + assert_proof_path_rejection(error); + } + // ----- multiple grids over one timestamp ------------------------------ + + use drive::query::TimeRangeGridSpec; + + const DAILY_INDEX: &str = "daily"; + const DAY_SECONDS: u64 = 24 * 3_600; + + /// The trending contract plus a second, daily (24h/24h) grid over the + /// same `$createdAt`. Grid-qualified level keys give each grid its own + /// subtree; the tests below pin that the wire can address each grid and + /// that the two prove independently. + fn register_two_grid_contract( + platform: &Platform, + platform_version: &PlatformVersion, + ) -> DataContract { + let factory = DataContractFactory::new(platform_version.protocol_version) + .expect("expected a factory"); + let schemas = platform_value!({ + DOCUMENT_TYPE: { + "type": "object", + "properties": { + "hashtag": { "type": "string", "maxLength": 63, "position": 0 }, + }, + "indices": [ + { + "name": BUCKETED_INDEX, + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "countable": true, + "timeRange": { "on": "$createdAt", "range": 21_600u64, "step": 7_200u64 }, + }, + { + "name": DAILY_INDEX, + "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], + "countable": true, + "timeRange": { "on": "$createdAt", "range": DAY_SECONDS, "step": DAY_SECONDS }, + }, + ], + "required": ["$createdAt", "hashtag"], + "additionalProperties": false, + } + }); + let contract = factory + .create_with_value_config(Identifier::new([9u8; 32]), 0, schemas, None, None) + .expect("a contract may bucket one timestamp with several grids") + .data_contract_owned(); + store_data_contract(platform, &contract, platform_version); + contract + } + + fn setup_two_grids( + platform: &TempPlatform, + base_state: &PlatformState, + platform_version: &PlatformVersion, + ) -> (DataContract, PlatformState) { + let contract = register_two_grid_contract(platform, platform_version); + insert_posts(platform, &contract, platform_version); + let state = state_with_committed_block_time(base_state, &platform.drive, platform_version); + (contract, state) + } + + /// The wire's structured `IN_TIME_RANGE` operand: + /// `[selector, range, step]` (seconds, as the contract declares them). + fn structured_where_clauses(hashtag: &str, grid: TimeRangeGridSpec) -> Vec { + let uint = |n: u64| ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::Uint64Value(n)), + }; + let mut values = vec![ + ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::Text( + TimeRangeSelector::Newest.as_str().to_string(), + )), + }, + uint(grid.range_seconds), + uint(grid.step_seconds), + ]; + if grid.phase_seconds != 0 { + values.push(uint(grid.phase_seconds)); + } + vec![ + wc( + "hashtag", + ProtoWhereOperator::Equal, + Value::Text(hashtag.to_string()), + ), + ProtoWhereClause { + field: CREATED_AT.to_string(), + operator: ProtoWhereOperator::InTimeRange as i32, + value: Some(ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::List( + document_field_value::ValueList { values }, + )), + }), + }, + ] + } + + const TRENDING_GRID: TimeRangeGridSpec = TimeRangeGridSpec { + range_seconds: 21_600, + step_seconds: 7_200, + phase_seconds: 0, + }; + const DAILY_GRID: TimeRangeGridSpec = TimeRangeGridSpec { + range_seconds: DAY_SECONDS, + step_seconds: DAY_SECONDS, + phase_seconds: 0, + }; + + /// With two grids on `$createdAt`, the bare text selector no longer + /// names a grid, so the handler refuses it as ambiguous rather than + /// picking one — a silent pick would prove an answer to a question the + /// client didn't ask. + #[test] + fn a_bare_selector_on_a_multi_grid_field_is_refused_as_ambiguous() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_two_grids(&platform, &base_state, version); + + let request = trending_request(contract.id().to_vec(), "ibiza", select_count_star()); + let result = platform + .query_documents_v1(request, &state, version) + .expect("transport-level success"); + assert!( + !result.errors.is_empty(), + "a bare selector over two grids must be refused" + ); + assert!( + format!("{:?}", result.errors).contains("grids"), + "the refusal must say the field is multi-grid: {:?}", + result.errors + ); + } + + /// A four-element operand spelling a zero phase is refused: like the + /// contract grammar and the storage key, zero is spelled by omission so + /// every grid has exactly one wire spelling. + #[test] + fn an_explicit_zero_phase_operand_is_refused_as_non_canonical() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_two_grids(&platform, &base_state, version); + + let mut clauses = structured_where_clauses("ibiza", TRENDING_GRID); + // append an explicit zero phase to the list operand + if let Some(ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::List(list)), + }) = clauses[1].value.as_mut() + { + list.values.push(ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::Uint64Value(0)), + }); + } else { + panic!("the helper builds a list operand"); + } + let request = GetDocumentsRequestV1 { + where_clauses: clauses, + ..trending_request(contract.id().to_vec(), "ibiza", select_count_star()) + }; + let result = platform + .query_documents_v1(request, &state, version) + .expect("transport-level success"); + assert!( + format!("{:?}", result.errors).contains("omission"), + "expected the one-spelling-per-grid refusal, got {:?}", + result.errors + ); + } + + /// Each grid proves and verifies independently through the SDK + /// `FromProof` entry point, with the structured operand naming the grid + /// on the wire and `with_time_range_grid` naming it in the client query. + /// The counts differ by design: the newest trending window holds two + /// `#ibiza` posts, while the newest daily window also contains the + /// one-hour-older post — three. A collapsed keyspace could not produce + /// both answers. + #[test] + fn each_grid_proves_and_verifies_independently_through_the_entry_point() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, state) = setup_two_grids(&platform, &base_state, version); + + for (grid, expected_count) in [(TRENDING_GRID, 2u64), (DAILY_GRID, 3u64)] { + let request = GetDocumentsRequestV1 { + where_clauses: structured_where_clauses("ibiza", grid), + ..trending_request(contract.id().to_vec(), "ibiza", select_count_star()) + }; + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let query = SdkDocumentQuery::new(Arc::new(contract.clone()), DOCUMENT_TYPE) + .expect("the fixture has this document type") + .with_select(SelectProjection::count_star()) + .with_where(WhereClause { + field: "hashtag".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("ibiza".to_string()), + }) + .with_time_range_grid(CREATED_AT, TimeRangeSelector::Newest, grid); + let (count, _mtd, _proof) = + >::maybe_from_proof_with_metadata( + query, + signed_response(proof, &mtd), + Network::Testnet, + version, + &provider, + ) + .expect("a correctly signed per-grid count must verify"); + assert_eq!( + count.expect("the bucket is not empty"), + DocumentCount(expected_count), + "grid {:?} must count its own bucket's members", + grid + ); + } + } + // ----- routes that must refuse time-range selections ------------------ + + use drive_proof_verifier::{DocumentHavingEntries, DocumentRankedEntries}; + + /// The HAVING route accepts equality prefixes that pin plain ranked + /// indexes, and its picker excludes transformed ones — so a resolved + /// bucket-start equality reaching it would be served from raw + /// timestamps at the bucket boundary instead of the selected window. + /// The server must refuse the combination outright (drive owns the + /// rejection, through `DocumentHavingRequest::resolved_time_ranges`). + #[test] + fn the_having_route_refuses_a_time_range_selection() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + + let request = GetDocumentsRequestV1 { + group_by: vec!["hashtag".to_string()], + having: vec![hc( + having_aggregate::Function::Count, + "", + having_clause::Operator::GreaterThan, + Value::U64(0), + )], + ..trending_request(contract.id().to_vec(), "ibiza", select_count_star()) + }; + let result = platform + .query_documents_v1(request, &state, version) + .expect("transport-level success"); + assert!( + format!("{:?}", result.errors).contains("time-range"), + "the HAVING route must refuse a time-range selection, got {:?}", + result.errors + ); + } + + /// The ranked and HAVING *verifiers* must refuse a time-range request + /// before authenticating anything: both surfaces pin plain ranked + /// indexes with equality prefixes, so a malicious node could otherwise + /// prove raw-timestamp matches at the bucket boundary against a request + /// shape every honest server rejects. The rejection fires ahead of + /// proof verification, so any signed response works as the fixture. + #[test] + fn ranked_and_having_verifiers_refuse_time_range_requests() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + let request = trending_request(contract.id().to_vec(), "ibiza", select_count_star()); + let (proof, mtd, provider) = prove_and_sign(&platform, &state, request, version); + + let time_range_query = || { + SdkDocumentQuery::new(Arc::new(contract.clone()), DOCUMENT_TYPE) + .expect("the fixture has this document type") + .with_select(SelectProjection::count_star()) + .with_time_range(CREATED_AT, TimeRangeSelector::Newest) + }; + + let error = + >::maybe_from_proof_with_metadata( + time_range_query(), + signed_response(proof.clone(), &mtd), + Network::Testnet, + version, + &provider, + ) + .expect_err("the ranked verifier must refuse a time-range request"); + assert!( + error.to_string().contains("time-range"), + "expected the ranked time-range refusal, got {error}" + ); + + let error = + >::maybe_from_proof_with_metadata( + time_range_query().with_having(vec![drive::query::HavingClause { + aggregate: drive::query::HavingAggregate { + function: drive::query::HavingAggregateFunction::Count, + field: String::new(), + }, + operator: drive::query::HavingOperator::GreaterThan, + right: drive::query::HavingRightOperand::Value(Value::U64(0)), + }]), + signed_response(proof, &mtd), + Network::Testnet, + version, + &provider, + ) + .expect_err("the HAVING verifier must refuse a time-range request"); + assert!( + error.to_string().contains("time-range"), + "expected the HAVING time-range refusal, got {error}" + ); + } + + /// A drive query carrying resolution provenance has no faithful + /// `DocumentQuery` form — serializing it would demote the resolved + /// bucket equality to a raw-timestamp predicate — so the conversion + /// must refuse rather than silently rewrite the question. + #[test] + fn a_resolved_drive_query_cannot_convert_to_a_document_query() { + let (platform, base_state, version) = setup_platform(None, Network::Testnet, None); + let (contract, _documents, state) = setup_trending(&platform, &base_state, version); + let document_type = contract + .document_type_for_name(DOCUMENT_TYPE) + .expect("post doctype exists"); + let block_time_ms = state + .last_committed_block_time_ms() + .expect("the fixture committed a block"); + let (clause, resolution) = resolve_time_range_bucket_clause( + CREATED_AT, + TimeRangeSelector::Newest, + None, + document_type, + block_time_ms, + ) + .expect("the committed block time is inside an active range"); + + let mut drive_query = DriveDocumentQuery::from_typed_clauses( + vec![clause], + Vec::new(), + None, + None, + true, + None, + &contract, + document_type, + &platform.config.drive, + version, + ) + .expect("the resolved clause builds a drive query"); + drive_query.resolved_time_ranges = vec![resolution]; + + let error = SdkDocumentQuery::try_from(&drive_query) + .expect_err("resolution provenance must not survive the conversion"); + assert!( + error.to_string().contains("provenance"), + "expected the provenance rejection, got {error}" + ); + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors_documents.rs b/packages/rs-drive-proof-verifier/tests/vectors_documents.rs index e6eb7753f3d..01e9ae063c0 100644 --- a/packages/rs-drive-proof-verifier/tests/vectors_documents.rs +++ b/packages/rs-drive-proof-verifier/tests/vectors_documents.rs @@ -150,6 +150,7 @@ fn document_query<'a>(case: &Case, contract: &'a DataContract) -> DriveDocumentQ start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], } } diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index 54012ec1f1f..1f505ea1aa6 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -52,12 +52,13 @@ 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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", optional = true, default-features = false } -grovedb-costs = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", optional = true } -grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } -grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", optional = true } -grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } -grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } +grovedb = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", optional = true, default-features = false } +grovedb-costs = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", optional = true } +grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } +grovedb-query = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } +grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", optional = true } +grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } +grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } [dev-dependencies] criterion = "0.5" diff --git a/packages/rs-drive/benches/document_count_worst_case.rs b/packages/rs-drive/benches/document_count_worst_case.rs index 40e8953bc81..9052996727f 100644 --- a/packages/rs-drive/benches/document_count_worst_case.rs +++ b/packages/rs-drive/benches/document_count_worst_case.rs @@ -1801,6 +1801,7 @@ fn display_proofs(fixture: &CountBenchFixture, platform_version: &PlatformVersio let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &case.structured, + &[], ) .expect("countable picker must find a covering index for the display case"); let query = DriveDocumentCountQuery { @@ -1818,6 +1819,7 @@ fn display_proofs(fixture: &CountBenchFixture, platform_version: &PlatformVersio let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &case.structured, + &[], ) .expect("range_countable picker must find a covering index"); let query = DriveDocumentCountQuery { @@ -2283,6 +2285,7 @@ fn count_request<'a>( limit, prove, drive_config: &fixture.drive_config, + resolved_time_ranges: vec![], } } diff --git a/packages/rs-drive/benches/document_sum_worst_case.rs b/packages/rs-drive/benches/document_sum_worst_case.rs index 6d42c21dbf8..cd4fdc7bff9 100644 --- a/packages/rs-drive/benches/document_sum_worst_case.rs +++ b/packages/rs-drive/benches/document_sum_worst_case.rs @@ -1602,6 +1602,7 @@ fn display_proofs(fixture: &SumBenchFixture, platform_version: &PlatformVersion) document_type, SUM_PROPERTY_NAME, &case.structured, + &[], platform_version, ) .expect("point-lookup path query builds"), @@ -1610,6 +1611,7 @@ fn display_proofs(fixture: &SumBenchFixture, platform_version: &PlatformVersion) document_type, SUM_PROPERTY_NAME, &case.structured, + &[], platform_version, ) .expect("aggregate-range path query builds"), @@ -1621,6 +1623,7 @@ fn display_proofs(fixture: &SumBenchFixture, platform_version: &PlatformVersion) document_type, SUM_PROPERTY_NAME, &case.structured, + &[], limit, left_to_right, platform_version, @@ -1914,6 +1917,7 @@ fn sum_request<'a>( limit, prove, drive_config: &fixture.drive_config, + resolved_time_ranges: vec![], } } diff --git a/packages/rs-drive/src/drive/contract/estimation_costs/add_estimation_costs_for_contract_insertion/v1/mod.rs b/packages/rs-drive/src/drive/contract/estimation_costs/add_estimation_costs_for_contract_insertion/v1/mod.rs index 907b0efb0df..5774dd6fa5d 100644 --- a/packages/rs-drive/src/drive/contract/estimation_costs/add_estimation_costs_for_contract_insertion/v1/mod.rs +++ b/packages/rs-drive/src/drive/contract/estimation_costs/add_estimation_costs_for_contract_insertion/v1/mod.rs @@ -12,7 +12,6 @@ use crate::error::Error; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::config::v0::DataContractConfigGettersV0; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods; use dpp::data_contract::DataContract; use dpp::serialization::PlatformSerializableWithPlatformVersion; @@ -25,7 +24,7 @@ use grovedb::EstimatedLayerCount::{ApproximateElements, EstimatedLevel}; use grovedb::EstimatedLayerSizes::{AllSubtrees, Mix}; use grovedb::EstimatedSumTrees::NoSumTrees; use grovedb::{EstimatedLayerInformation, TreeType}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; impl Drive { /// v1 of contract-insertion cost estimation. Differs from v0 by computing @@ -134,17 +133,14 @@ impl Drive { let mut tree_weights = TreeTypeWeights::default(); tree_weights.tally(pk_tree_type); + // One root sub-level per distinct top tree (grid-qualified keys + // for time-range first properties), matching the trees + // `insert_contract_v0` actually creates. The map is already + // deduped. let index_structure = document_type_ref.index_structure(); - let mut seen_indexes: HashSet<&[u8]> = HashSet::new(); - for index in document_type_ref.top_level_indices() { - let index_bytes = index.name.as_bytes(); - if !seen_indexes.insert(index_bytes) { - continue; - } - let terminator_tree_type = index_structure - .sub_levels() - .get(index.name.as_str()) - .and_then(|level| level.has_index_with_type()) + for level in index_structure.sub_levels().values() { + let terminator_tree_type = level + .has_index_with_type() .map(property_name_tree_type_from_flags) .unwrap_or(TreeType::NormalTree); tree_weights.tally(terminator_tree_type); 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 42aeab8eaec..20216b04eb6 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 @@ -15,7 +15,6 @@ use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; -use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods; use dpp::serialization::PlatformSerializableWithPlatformVersion; use crate::drive::votes::paths::{ @@ -25,7 +24,7 @@ use crate::error::contract::DataContractError; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; impl Drive { /// Insert a contract. @@ -361,14 +360,18 @@ impl Drive { )?, } - let mut index_cache: HashSet<&[u8]> = HashSet::new(); let document_type_ref = document_type.as_ref(); let index_structure = document_type_ref.index_structure(); - // for each type we should insert the indices that are top level - for index in document_type.as_ref().top_level_indices() { - // toDo: change this to be a reference by index - let index_bytes = index.name.as_bytes(); - if !index_cache.contains(index_bytes) { + // For each type we should insert the indices that are top level. + // The index structure's root sub-levels are exactly the distinct + // top-level trees: one per plain first property, plus one per + // (property, grid) pair for time-range-transformed first + // properties, whose keys are already grid-qualified + // (`TimeRangeTransform::storage_key`). Iterating the map also + // dedupes indexes sharing a first level for free. + for (level_key, level) in index_structure.sub_levels() { + let index_bytes = level_key.as_bytes(); + { // The property-name tree variant (the tree at // `@/contract/0x01//`) is selected from // the index's `(range_countable, range_summable)` @@ -407,10 +410,7 @@ impl Drive { // 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 index_info = level.has_index_with_type(); let (tree_type, ranked_axes) = property_name_tree_type_and_ranked_axes(index_info)?; match tree_type { @@ -472,7 +472,6 @@ impl Drive { &platform_version.drive, )?, } - index_cache.insert(index_bytes); } } } diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs index d81d73c4267..a158e287623 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs @@ -317,6 +317,7 @@ fn run( offset: Some(0), has_start_at: false, prove, + resolved_time_ranges: &[], }, None, platform_version(), diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/range_countable_index_e2e_tests.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/range_countable_index_e2e_tests.rs index 277da24108c..e97037cc5b2 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/range_countable_index_e2e_tests.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/range_countable_index_e2e_tests.rs @@ -567,6 +567,7 @@ fn range_count_executor_sums_and_splits_correctly() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("range_countable index should be picked"); @@ -647,6 +648,7 @@ fn range_count_executor_sums_and_splits_correctly() { let after_index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &after_clauses, + &[], ) .expect("range_countable index should be picked"); let after_query = DriveDocumentCountQuery { @@ -747,6 +749,7 @@ fn range_count_executor_between_is_inclusive_on_both_bounds() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("range_countable index should be picked"); @@ -844,6 +847,7 @@ fn aggregate_count_proof_verifies_and_returns_correct_count() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("range_countable index should be picked"); @@ -998,6 +1002,7 @@ fn range_count_with_in_on_prefix_returns_per_brand_color_entries() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("range_countable index should be picked"); @@ -1151,6 +1156,7 @@ fn range_count_executor_accepts_starts_with_in_all_four_modes() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("picker accepts StartsWith"); let query = DriveDocumentCountQuery { @@ -1296,6 +1302,7 @@ fn range_count_executor_accepts_empty_starts_with_prefix_via_sentinel() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("picker accepts StartsWith with any value"); let query = DriveDocumentCountQuery { @@ -1418,6 +1425,7 @@ fn assert_aggregate_count_proof_returns( let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("range_countable index should be picked"); @@ -1715,6 +1723,7 @@ fn aggregate_count_proof_verifies_on_compound_index_with_equal_prefix() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("compound range_countable index should be picked"); @@ -1879,6 +1888,7 @@ fn aggregate_count_proof_counts_cars_in_parking_lots_greater_than_b() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("byLot range_countable index should be picked"); let query = DriveDocumentCountQuery { @@ -2135,6 +2145,7 @@ fn range_count_executor_returns_per_lot_counts_for_lots_greater_than_b() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("byLot range_countable index should be picked"); @@ -2309,6 +2320,7 @@ fn distinct_count_proof_returns_per_lot_counts_for_lots_greater_than_b() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("byLot range_countable index should be picked"); @@ -2654,6 +2666,7 @@ fn distinct_count_proof_honors_request_limit() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("byLot picked"); let query = DriveDocumentCountQuery { @@ -2812,6 +2825,7 @@ fn distinct_count_proof_descending_returns_last_limit_keys() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("byLot picked"); let query = DriveDocumentCountQuery { @@ -2936,6 +2950,7 @@ fn distinct_count_proof_rejects_limit_above_max_query_limit() { limit: Some(too_large), prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let result = drive.execute_document_count_request(request, None, pv); @@ -3081,6 +3096,7 @@ fn distinct_count_proof_with_in_on_prefix_returns_per_brand_color_entries() { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("byBrandColor picked"); let query = DriveDocumentCountQuery { 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 index 390621a9886..d59306b7e26 100644 --- 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 @@ -72,7 +72,10 @@ 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; +use grovedb::element::IndexAxis; +use grovedb::query_result_type::QueryResultType; +use grovedb::{AxisKeys, Element, PathQuery, PathQueryRun}; +use grovedb_query::AxisQuery; /// The one index property every doctype in the fixture ranks by. const GROUP_PROPERTY: &str = "restaurantId"; @@ -271,49 +274,57 @@ fn group_keys(entries: &[(T, Vec)]) -> Vec { .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 +/// Run a keys-only top-k axis read through the unified PathQuery +/// surface — the same route the ranked no-proof executor takes. +fn run_top_k_keys( + drive: &Drive, + path: &[Vec], + axis: IndexAxis, + k: u16, + descending: bool, +) -> AxisKeys { + let path_query = PathQuery::new_axis( + path.to_vec(), + AxisQuery::top_k(axis, k, 0, descending).keys_only(), + ); + match drive .grove - .indexed_avg_top_k_keys( - path_refs.as_slice(), - k, - descending, + .run_path_query( + &path_query, + true, + true, + true, + QueryResultType::QueryPathKeyElementTrioResultType, None, &platform_version().drive.grove_version, ) .unwrap() - .expect("indexed_avg_top_k_keys must succeed") + .expect("top-k axis read must succeed") + { + PathQueryRun::AxisKeys { keys, .. } => keys, + other => panic!("expected AxisKeys, got {other:?}"), + } +} + +fn avg_top_k(drive: &Drive, path: &[Vec], k: u16, descending: bool) -> Vec<(i128, Vec)> { + match run_top_k_keys(drive, path, IndexAxis::Avg, k, descending) { + AxisKeys::Avg(pairs) => pairs, + other => panic!("expected avg pairs, got {other:?}"), + } } 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_keys( - path_refs.as_slice(), - k, - descending, - None, - &platform_version().drive.grove_version, - ) - .unwrap() - .expect("indexed_count_top_k_keys must succeed") + match run_top_k_keys(drive, path, IndexAxis::Count, k, descending) { + AxisKeys::Count(pairs) => pairs, + other => panic!("expected count pairs, got {other:?}"), + } } 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_keys( - path_refs.as_slice(), - k, - descending, - None, - &platform_version().drive.grove_version, - ) - .unwrap() - .expect("indexed_sum_top_k_keys must succeed") + match run_top_k_keys(drive, path, IndexAxis::Sum, k, descending) { + AxisKeys::Sum(pairs) => pairs, + other => panic!("expected sum pairs, got {other:?}"), + } } // --------------------------------------------------------------------------- @@ -795,6 +806,7 @@ fn compound_ranked_index_resolves_its_terminal_level_to_an_indexed_tree() { ranked_countable: false, ranked_summable: false, ranked_averageable: true, + time_range: None, }; let index_structure = IndexLevel::try_from_indices([&compound_ranked_index], "dish", platform_version()) @@ -1010,6 +1022,7 @@ fn a_null_unsearchable_ranked_level_is_what_makes_a_phantom_group_possible() { ranked_countable: true, ranked_summable: false, ranked_averageable: false, + time_range: None, }; for null_searchable in [false, true] { @@ -1505,6 +1518,7 @@ fn ranked_avg_page( offset: Some(offset), has_start_at: false, prove: false, + resolved_time_ranges: &[], }, None, platform_version(), @@ -1551,6 +1565,7 @@ fn verified_ranked_avg_page( offset: Some(offset), has_start_at: false, prove: true, + resolved_time_ranges: &[], }, None, platform_version(), 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 7366e1e9a0d..234496ea64e 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 @@ -15,14 +15,13 @@ use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; -use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods; use dpp::serialization::PlatformSerializableWithPlatformVersion; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; impl Drive { /// Updates a data contract. @@ -301,10 +300,13 @@ impl Drive { type_key.as_bytes(), ]; - let mut index_cache: HashSet<&[u8]> = HashSet::new(); let document_type_ref = document_type.as_ref(); let index_structure = document_type_ref.index_structure(); - // for each type we should insert the indices that are top level. + // For each type we should insert the indices that are top + // level — one root sub-level per distinct top tree (plain + // first properties by name, time-range grids by their + // qualified `TimeRangeTransform::storage_key`), the same + // iteration `insert_contract_v0` performs. // // `batch_insert_empty_tree_if_not_exists` is a no-op when the // index already exists, so this loop covers BOTH the @@ -320,13 +322,9 @@ impl Drive { // contract update silently created a NormalTree, diverging // from the layout a fresh insert would have produced and // breaking subsequent range-sum / range-count reads. - for index in document_type.as_ref().top_level_indices() { - let index_bytes = index.name.as_bytes(); - if !index_cache.contains(index_bytes) { - let index_info = index_structure - .sub_levels() - .get(index.name.as_str()) - .and_then(|level| level.has_index_with_type()); + for (level_key, level) in index_structure.sub_levels() { + { + let index_info = level.has_index_with_type(); // 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 @@ -356,7 +354,7 @@ impl Drive { // indexes (unchanged on disk) and brand-new ones // (materialized with the dispatch-chosen variant). self.batch_insert_empty_index_tree_if_not_exists( - PathFixedSizeKeyRef((type_path, index.name.as_bytes())), + PathFixedSizeKeyRef((type_path, level_key.as_bytes())), target_tree_type, &ranked_axes, storage_flags.as_ref().map(|flags| flags.as_ref()), @@ -366,7 +364,6 @@ impl Drive { &mut batch_operations, drive_version, )?; - index_cache.insert(index_bytes); } } } else { @@ -461,13 +458,15 @@ impl Drive { )?, } - let mut index_cache: HashSet<&[u8]> = HashSet::new(); let document_type_ref = document_type.as_ref(); let index_structure = document_type_ref.index_structure(); - // for each type we should insert the indices that are top level - for index in document_type.as_ref().top_level_indices() { - let index_bytes = index.name.as_bytes(); - if !index_cache.contains(index_bytes) { + // For each type we should insert the indices that are top + // level — the index structure's root sub-levels, whose keys + // are grid-qualified for time-range first properties (see + // `insert_contract_v0`). + for (level_key, level) in index_structure.sub_levels() { + let index_bytes = level_key.as_bytes(); + { // Top-level index tree variant is selected from the // index's `(range_countable, range_summable)` pair — // identical 4-way dispatch as @@ -477,10 +476,7 @@ impl Drive { // sum- or range-countable top-level index added via // contract update, diverging on-disk layout from // fresh-insert contracts. - let index_info = index_structure - .sub_levels() - .get(index.name.as_str()) - .and_then(|level| level.has_index_with_type()); + let index_info = level.has_index_with_type(); let (tree_type, ranked_axes) = property_name_tree_type_and_ranked_axes(index_info)?; match tree_type { @@ -542,7 +538,6 @@ impl Drive { drive_version, )?, } - index_cache.insert(index_bytes); } } } 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 884cf5306d5..f95cd2c06e4 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 @@ -8,12 +8,16 @@ use grovedb::EstimatedSumTrees::NoSumTrees; use std::collections::HashMap; use crate::drive::document::estimation_costs::estimated_sum_trees_for_value_tree_type::estimated_sum_trees_for_value_tree_type; -use crate::drive::document::index_level_tree_types::index_level_tree_types_with_continuation_demotion; +use crate::drive::document::index_level_tree_types::{ + index_level_tree_types_with_continuation_demotion, time_range_index_keys, +}; use crate::drive::document::unique_event_id; use crate::util::type_constants::DEFAULT_HASH_SIZE_U8; use crate::drive::Drive; -use crate::util::object_size_info::{DocumentAndContractInfo, DocumentInfoV0Methods, PathInfo}; +use crate::util::object_size_info::{ + DocumentAndContractInfo, DocumentInfoV0Methods, DriveKeyInfo, PathInfo, +}; use crate::error::fee::FeeError; use crate::error::Error; @@ -108,13 +112,23 @@ impl Drive { let mut index_path: Vec> = contract_document_type_path.clone(); index_path.push(Vec::from(name.as_bytes())); + // The level key is the path segment; the document value is read + // from the *source property* — they differ on a time-range + // level, whose key is grid-qualified + // (`TimeRangeTransform::storage_key`) while the timestamp lives + // under the bare property name. Mirrors the insert walker. + let property_name = sub_level + .time_range() + .map(|transform| transform.source.as_str()) + .unwrap_or(name.as_str()); + // with the example of the dashpay contract's first index // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId let document_top_field = document_and_contract_info .owned_document_info .document_info .get_raw_for_document_type( - name, + property_name, document_type, document_and_contract_info.owned_document_info.owner_id, Some((sub_level, event_id)), @@ -127,7 +141,11 @@ impl Drive { let document_top_field_estimated_size = document_and_contract_info .owned_document_info .document_info - .get_estimated_size_for_document_type(name, document_type, platform_version)?; + .get_estimated_size_for_document_type( + property_name, + document_type, + platform_version, + )?; if document_top_field_estimated_size > u8::MAX as u16 { return Err(Error::Fee(FeeError::Overflow( @@ -155,36 +173,67 @@ impl Drive { let any_fields_null = document_top_field.is_empty(); let all_fields_null = document_top_field.is_empty(); - let mut index_path_info = if document_and_contract_info - .owned_document_info - .document_info - .is_document_size() - { - // This is a stateless operation - PathInfo::PathWithSizes(KeyInfoPath::from_known_owned_path(index_path)) - } else { - PathInfo::PathAsVec::<0>(index_path) - }; + // Mirror the insert side's time-range fan-out: a time-range + // first-property node removes one index entry per overlapping + // range bucket the document's timestamp fell into. The keys are + // recomputed deterministically through the same shared helper the + // insert walker uses, so they match exactly what insert wrote — + // including the null case (single null entry) and the pre-origin + // case (no entries on either side). + let index_keys: Vec = time_range_index_keys( + sub_level.time_range(), + document_top_field, + // A validated contract cannot exceed this; the clamp only + // bounds estimation work for unvalidated transforms. The + // `unwrap_or(1)` arm is a protocol version without + // time-range indexes, where no transform can exist. + platform_version + .system_limits + .max_time_range_overlap_factor + .unwrap_or(1), + ); - // we push the actual value of the index path - index_path_info.push(document_top_field)?; - // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId/ - - self.remove_indices_for_index_level_for_contract_operations( - document_and_contract_info, - index_path_info, - sub_level, - any_fields_null, - all_fields_null, - value_tree_type, - &storage_flags, - previous_batch_operations, - estimated_costs_only_with_layer_info, - event_id, - transaction, - batch_operations, - platform_version, - )?; + let bucket_count = index_keys.len(); + for (bucket, index_key) in index_keys.into_iter().enumerate() { + // The final bucket takes ownership of `index_path`; earlier + // buckets (only a time-range fan-out has more than one) + // clone it. + let own_index_path = if bucket + 1 == bucket_count { + std::mem::take(&mut index_path) + } else { + index_path.clone() + }; + let mut index_path_info = if document_and_contract_info + .owned_document_info + .document_info + .is_document_size() + { + // This is a stateless operation + PathInfo::PathWithSizes(KeyInfoPath::from_known_owned_path(own_index_path)) + } else { + PathInfo::PathAsVec::<0>(own_index_path) + }; + + // we push the actual value of the index path + index_path_info.push(index_key)?; + // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId/ + + self.remove_indices_for_index_level_for_contract_operations( + document_and_contract_info, + index_path_info, + sub_level, + any_fields_null, + all_fields_null, + value_tree_type, + &storage_flags, + previous_batch_operations, + estimated_costs_only_with_layer_info, + event_id, + transaction, + batch_operations, + platform_version, + )?; + } } Ok(()) } 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 a5c9303e902..e6f312c00e7 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 @@ -75,7 +75,9 @@ 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 crate::util::object_size_info::DriveKeyInfo; +use dpp::data_contract::document_type::{IndexLevel, TimeRangeTransform}; +use grovedb::batch::key_info::KeyInfo; use grovedb::element::IndexAxis; use grovedb::TreeType; @@ -128,6 +130,69 @@ pub(crate) fn index_level_tree_types_with_continuation_demotion( }) } +/// Expands a document's raw top-field key into the set of index-entry keys a +/// time-range first-property node stores it under. For a node without a +/// transform the single key passes through untouched. +/// +/// Shared by the insert and delete v2 walkers (same must-not-drift contract +/// as the tree-type derivation above); the entry-key rule itself — null keeps +/// its single null entry, epoch-sliver timestamps produce no entries, +/// undecodable values keep their raw key — lives in +/// [`TimeRangeTransform::entry_keys_for_raw`], which the update walker also +/// calls. +/// +/// On the estimated-cost path (`KeySize`) the real timestamp isn't available, +/// so this assumes the worst case of `overlap_factor` overlapping buckets — +/// and makes each worst-case key **distinct** by suffixing an ordinal: +/// identical `(path, key)` operations collapse inside grovedb's batch +/// structure, so `overlap` copies of one key would silently estimate a single +/// bucket's cost. +/// +/// `max_overlap_factor` is the platform version's +/// `SystemLimits::max_time_range_overlap_factor` — a validated contract can +/// never exceed it, so the clamp only bounds estimation work for a transform +/// built outside validation, and reading it from the version keeps the +/// estimated fan-out in step with whatever a future protocol version allows. +pub(crate) fn time_range_index_keys<'a>( + transform: Option<&TimeRangeTransform>, + document_top_field: DriveKeyInfo<'a>, + max_overlap_factor: u64, +) -> Vec> { + let Some(transform) = transform else { + return vec![document_top_field]; + }; + match &document_top_field { + DriveKeyInfo::KeySize(key_info) => { + // Not `clamp(1, max)`: `Ord::clamp` asserts `min <= max`, so a + // future limits table carrying `Some(0)` would panic here. + let overlap = transform.overlap_factor().min(max_overlap_factor).max(1) as usize; + (0..overlap) + .map(|ordinal| { + let mut key_info = key_info.clone(); + let suffix = (ordinal as u16).to_be_bytes(); + match &mut key_info { + KeyInfo::KnownKey(bytes) => bytes.extend_from_slice(&suffix), + KeyInfo::MaxKeySize { unique_id, .. } => { + unique_id.extend_from_slice(&suffix) + } + } + DriveKeyInfo::KeySize(key_info) + }) + .collect() + } + DriveKeyInfo::Key(raw) => transform + .entry_keys_for_raw(raw) + .into_iter() + .map(DriveKeyInfo::Key) + .collect(), + DriveKeyInfo::KeyRef(raw) => transform + .entry_keys_for_raw(raw) + .into_iter() + .map(DriveKeyInfo::Key) + .collect(), + } +} + /// 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 @@ -329,6 +394,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: true, + time_range: None, }; let compound = Index { name: "byRestaurantChef".to_string(), @@ -352,6 +418,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }; let index_structure = @@ -394,4 +461,66 @@ mod tests { ) .expect("the continuation must be insertable under the demoted value tree"); } + /// The estimated-cost (`KeySize`) branch of [`time_range_index_keys`] is + /// consensus-sensitive fee math: it must emit exactly the bounded + /// overlap count, keep every synthetic key's `max_size` untouched, and + /// make each `unique_id` distinct — grovedb's batch structure collapses + /// identical `(path, key)` operations, so `overlap` copies of one key + /// would silently estimate a single bucket's cost. + #[test] + fn estimated_time_range_fan_out_emits_distinct_worst_case_keys() { + use super::time_range_index_keys; + use crate::util::object_size_info::DriveKeyInfo; + use dpp::data_contract::document_type::TimeRangeTransform; + use grovedb::batch::key_info::KeyInfo; + + // 6h window sliding every 2h — overlap factor 3. + let transform = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 21_600, + step_seconds: 7_200, + phase_seconds: 0, + }; + let key = DriveKeyInfo::KeySize(KeyInfo::MaxKeySize { + unique_id: vec![7u8; 4], + max_size: 8, + }); + + let keys = time_range_index_keys(Some(&transform), key.clone(), 24); + assert_eq!(keys.len(), 3, "one worst-case key per overlapping bucket"); + let mut unique_ids = Vec::new(); + for entry in &keys { + let DriveKeyInfo::KeySize(KeyInfo::MaxKeySize { + unique_id, + max_size, + }) = entry + else { + panic!("the KeySize branch must stay on the estimation path"); + }; + assert_eq!( + *max_size, 8, + "the ordinal suffix disambiguates unique_id only; the estimated \ + key size must be the timestamp key's" + ); + unique_ids.push(unique_id.clone()); + } + let distinct: std::collections::BTreeSet<_> = unique_ids.iter().collect(); + assert_eq!( + distinct.len(), + 3, + "identical unique_ids would collapse in the batch and under-estimate" + ); + + // An unvalidated transform above the version's cap is clamped: the + // estimation work stays bounded by what the protocol version allows. + let oversized = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 100 * 3_600, + step_seconds: 3_600, + phase_seconds: 0, + }; + assert_eq!(oversized.overlap_factor(), 100); + let keys = time_range_index_keys(Some(&oversized), key, 24); + assert_eq!(keys.len(), 24, "fan-out must clamp to the versioned cap"); + } } diff --git a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs index 7612dab7976..b649ced298c 100644 --- a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs @@ -185,6 +185,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation diff --git a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs index 4b5ea32f058..2c3572be562 100644 --- a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs @@ -4,13 +4,16 @@ use crate::drive::document::index_uniqueness::internal::validate_uniqueness_of_d UniquenessOfDataRequestUpdateType, UniquenessOfDataRequestV1, }; use crate::drive::document::query::QueryDocumentsOutcomeV0Methods; +use crate::error::drive::DriveError; use crate::error::Error; -use crate::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; +use crate::query::{ + DriveDocumentQuery, InternalClauses, ResolvedTimeRange, WhereClause, WhereOperator, +}; use dpp::consensus::state::document::duplicate_unique_index_error::DuplicateUniqueIndexError; use dpp::consensus::state::state_error::StateError; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::{property_names, DocumentV0Getters}; -use dpp::platform_value::platform_value; +use dpp::platform_value::{platform_value, Value}; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use grovedb::TransactionArg; @@ -70,7 +73,7 @@ impl Drive { // if an index is not unique there is no issue None } else { - let (where_queries, allow_original) = match &update_type { + let (mut where_queries, allow_original) = match &update_type { UniquenessOfDataRequestUpdateType::NewDocument => { let where_queries = index .properties @@ -352,6 +355,93 @@ impl Drive { // there are empty fields, which means that the index is no longer unique None } else { + // A unique time-range index stores bucket *starts* + // under its first property, never raw timestamps, so + // probing it with the candidate document's own + // timestamp would look in a key that no document ever + // occupies and report every duplicate as unique. + // Rewrite the source equality to the containing + // bucket, and record the rewrite as provenance so + // index selection admits the bucketed index (see + // `index_admissible_for_resolved_time_range`): this is + // a legitimate internal producer of a resolved bucket + // equality — the value is derived deterministically + // from the candidate document's own timestamp through + // the contract's transform, so every node computes the + // identical clause. + // + // On the `ChangedDocument` path this stays correct + // without any old-vs-new bucket tracking (which the + // request has no field for — note the arm carries no + // `changed_created_at` flag): a unique time-range + // index is validated to bucket `$createdAt`, which is + // immutable across updates, so the bucket component of + // the tuple never moves and `allow_original` keeps its + // meaning — the tuple changed exactly when one of the + // index's other properties changed. + let mut resolved_time_ranges = Vec::new(); + if let Some(transform) = &index.time_range { + let Some(clause) = where_queries.get_mut(transform.source.as_str()) + else { + // Unreachable: the transform's source is + // validated to be the index's first property, + // and the count check above established that + // every property produced a clause. + return Some(Err(Error::Drive( + DriveError::CorruptedCodeExecution( + "a time-range index's source must be one of its \ + properties", + ), + ))); + }; + // The clause value was built by `platform_value!` + // from an `Option`, so it is a + // `U64`; `I64` is accepted defensively because a + // non-system-timestamp source would arrive through + // the document data map. Anything else is + // unreachable under a validated contract and must + // fail loudly: silently skipping this index's + // check would let the collision surface later as + // a corrupted-index insert error, because the + // write path stores a non-timestamp value under + // its raw key rather than dropping it. + let timestamp = + match &clause.value { + Value::U64(timestamp) => *timestamp, + Value::I64(timestamp) => match u64::try_from(*timestamp) { + Ok(timestamp) => timestamp, + Err(_) => return Some(Err(Error::Drive( + DriveError::CorruptedCodeExecution( + "a unique time-range index's source value must \ + be a millisecond timestamp", + ), + ))), + }, + _ => { + return Some(Err(Error::Drive( + DriveError::CorruptedCodeExecution( + "a unique time-range index's source value must be \ + a millisecond timestamp", + ), + ))) + } + }; + // A validated unique time-range index has overlap + // factor 1 (range == step), so any real timestamp + // yields exactly one containing bucket. An empty + // result means the timestamp falls in the + // sub-`step` epoch sliver before the grid's phase + // anchor: such documents produce no index entries + // at all, so they cannot collide with anything + // under this index and the whole check is skipped + // for it. + let bucket_start = *transform.containing_buckets(timestamp).first()?; + clause.value = platform_value!(bucket_start); + resolved_time_ranges.push(ResolvedTimeRange { + transform: transform.clone(), + }); + } + let query = DriveDocumentQuery { contract, document_type, @@ -368,6 +458,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges, }; // todo: deal with cost of this operation diff --git a/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs b/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs index 5e1f8f3672d..faf6228b017 100644 --- a/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs +++ b/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs @@ -700,3 +700,485 @@ mod tests { ); } } + +#[cfg(test)] +#[cfg(feature = "server")] +mod unique_time_range_index_tests { + //! A unique time-range index expresses "at most one document per + //! non-overlapping window per remaining key tuple" — one report per author + //! per day. Its uniqueness probe therefore has to look in the *bucket* the + //! candidate's `$createdAt` falls into: the index stores bucket starts, not + //! raw timestamps, so a probe on the raw value would look at a key no + //! document ever occupies and pronounce every duplicate unique. + //! + //! The rewritten equality is indistinguishable from a client-written one + //! once built, so the probe must also carry the resolution provenance — + //! without it index selection refuses to route a `$createdAt` equality to a + //! bucketed index and the probe would fail to find any index at all. + use std::borrow::Cow; + use std::collections::{BTreeMap, BTreeSet}; + + use dpp::block::block_info::BlockInfo; + use dpp::consensus::state::state_error::StateError; + use dpp::consensus::ConsensusError; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::DataContractFactory; + use dpp::document::{Document, DocumentV0, DocumentV0Getters}; + use dpp::identifier::Identifier; + use dpp::platform_value::{platform_value, Value}; + use dpp::prelude::DataContract; + use dpp::validation::SimpleConsensusValidationResult; + use dpp::version::PlatformVersion; + + use crate::drive::document::index_uniqueness::internal::validate_uniqueness_of_data::{ + UniquenessOfDataRequest, UniquenessOfDataRequestUpdateType, UniquenessOfDataRequestV1, + }; + use crate::drive::Drive; + 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; + + /// One day in each of the two units these tests deal in: the contract + /// declares its window in seconds, while every `$createdAt` and every + /// bucket start is a millisecond timestamp. + const DAY_SECONDS: u64 = 24 * 3_600; + const DAY_MS: u64 = 24 * 3_600_000; + + /// Deterministic 32-byte fixture identifier derived from the document's + /// own fixture inputs. Identifiers here are plumbing, not test inputs: + /// fixed bytes keep a failing GroveDB fixture reproducible run-to-run and + /// avoid an OS-entropy dependency (and its unwrap) in + /// consensus-sensitive tests. `marker` separates namespaces (document id + /// vs owner) and same-timestamp siblings. + fn fixture_bytes(marker: u8, created_at: u64, tag: &str) -> [u8; 32] { + let mut bytes = [0u8; 32]; + bytes[0] = marker; + bytes[1..9].copy_from_slice(&created_at.to_be_bytes()); + for (i, byte) in tag.bytes().take(23).enumerate() { + bytes[9 + i] = byte; + } + bytes + } + /// Start of the window every "same window" timestamp below lands in — + /// a whole number of days, so it is a bucket start on the default + /// (phase 0) daily grid. + const WINDOW_MS: u64 = 100 * DAY_MS; + /// A half-day phase for the phased-grid test: bucket starts move to + /// `k * day + 12h`, and the only timestamps outside every window are the + /// first twelve hours of 1970 — the epoch sliver the probe must skip. + const PHASE_SECONDS: u64 = 12 * 3_600; + const PHASE_MS: u64 = 12 * 3_600_000; + + /// A `report` document type with a UNIQUE + /// `(timeRange($createdAt, range = step = 1 day), author)` index: one + /// report per author per calendar day. Both index properties are required, + /// so neither can be null and the terminator always takes the unique + /// layout. + /// + /// `phase_seconds` shifts the window grid within one step (validation + /// requires `phase < step`); `None` leaves it at the default (0). + fn build_unique_daily_report_contract(phase_seconds: Option) -> DataContract { + let factory = + DataContractFactory::new(PlatformVersion::latest().protocol_version).expect("factory"); + let mut time_range = vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + (Value::Text("range".to_string()), Value::U64(DAY_SECONDS)), + (Value::Text("step".to_string()), Value::U64(DAY_SECONDS)), + ]; + if let Some(phase_seconds) = phase_seconds { + time_range.push((Value::Text("phase".to_string()), Value::U64(phase_seconds))); + } + let index_map = vec![ + ( + Value::Text("name".to_string()), + Value::Text("dailyReport".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"$createdAt": "asc"}), + platform_value!({"author": "asc"}), + ]), + ), + (Value::Text("unique".to_string()), Value::Bool(true)), + (Value::Text("timeRange".to_string()), Value::Map(time_range)), + ]; + + let document_schema = platform_value!({ + "type": "object", + "properties": { + "author": {"type": "string", "maxLength": 63, "position": 0}, + }, + "required": ["author", "$createdAt"], + "indices": Value::Array(vec![Value::Map(index_map)]), + "additionalProperties": false, + }); + let schemas = platform_value!({ "report": document_schema }); + factory + .create_with_value_config(Identifier::from([201u8; 32]), 0, schemas, None, None) + .expect("create contract") + .data_contract_owned() + } + + fn setup(platform_version: &'static PlatformVersion) -> (Drive, DataContract) { + setup_with_phase(None, platform_version) + } + + fn setup_with_phase( + phase_seconds: Option, + platform_version: &'static PlatformVersion, + ) -> (Drive, DataContract) { + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_unique_daily_report_contract(phase_seconds); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + (drive, contract) + } + + /// Stores a `report` and returns its id. + fn insert_report( + drive: &Drive, + contract: &DataContract, + created_at: u64, + author: &str, + platform_version: &PlatformVersion, + ) -> Identifier { + let document_type = contract.document_type_for_name("report").expect("report"); + let owner_bytes = fixture_bytes(1, created_at, author); + let document = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(2, created_at, author)), + owner_id: Identifier::from(owner_bytes), + properties: BTreeMap::from([("author".to_string(), Value::Text(author.to_string()))]), + created_at: Some(created_at), + revision: Some(1), + ..Default::default() + }); + let document_id = document.id(); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(owner_bytes), + }, + contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add document"); + document_id + } + + /// Runs the uniqueness validation for a candidate `report`. + fn check_uniqueness( + drive: &Drive, + contract: &DataContract, + document_id: Identifier, + created_at: u64, + author: &str, + update_type: UniquenessOfDataRequestUpdateType, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + let document_type = contract.document_type_for_name("report").expect("report"); + let data = BTreeMap::from([("author".to_string(), Value::Text(author.to_string()))]); + let request = UniquenessOfDataRequestV1 { + contract, + document_type, + owner_id: Identifier::from([0x01; 32]), + creator_id: None, + document_id, + created_at: Some(created_at), + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + data: &data, + update_type, + }; + drive + .validate_uniqueness_of_data( + UniquenessOfDataRequest::V1(request), + None, + platform_version, + ) + .expect("uniqueness validation should run") + } + + fn assert_duplicate(result: &SimpleConsensusValidationResult, context: &str) { + assert!( + matches!( + result.errors.first(), + Some(ConsensusError::StateError( + StateError::DuplicateUniqueIndexError(_) + )) + ), + "{context}: expected a DuplicateUniqueIndexError, got: {:?}", + result.errors + ); + } + + /// The core of the feature: two documents whose raw `$createdAt` values + /// differ by hours still occupy the same day bucket, so the second one + /// violates the unique index. A probe that compared raw timestamps would + /// see an empty index and let it through. + #[test] + fn candidate_in_the_same_window_as_a_stored_document_is_a_duplicate() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); + + insert_report( + &drive, + &contract, + WINDOW_MS + 3_600_000, + "alice", + platform_version, + ); + + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + // 8 hours later — a different timestamp, the same day bucket. + WINDOW_MS + 9 * 3_600_000, + "alice", + UniquenessOfDataRequestUpdateType::NewDocument, + platform_version, + ); + assert_duplicate(&result, "same window, same author"); + } + + /// The next window is a different bucket, so the same author may report + /// again — and the write actually goes through, proving the probe agrees + /// with the layout the insert walker builds. + #[test] + fn candidate_in_the_next_window_is_unique_and_insertable() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); + + insert_report( + &drive, + &contract, + WINDOW_MS + 3_600_000, + "alice", + platform_version, + ); + + let next_window_timestamp = WINDOW_MS + DAY_MS + 3_600_000; + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + next_window_timestamp, + "alice", + UniquenessOfDataRequestUpdateType::NewDocument, + platform_version, + ); + assert!( + result.is_valid(), + "the next window is a different bucket: {:?}", + result.errors + ); + + insert_report( + &drive, + &contract, + next_window_timestamp, + "alice", + platform_version, + ); + } + + /// The bucket is only the first component of the tuple: a different author + /// in the same window is a different slot. + #[test] + fn candidate_in_the_same_window_with_a_different_suffix_is_unique() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); + + insert_report( + &drive, + &contract, + WINDOW_MS + 3_600_000, + "alice", + platform_version, + ); + + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + WINDOW_MS + 9 * 3_600_000, + "bob", + UniquenessOfDataRequestUpdateType::NewDocument, + platform_version, + ); + assert!( + result.is_valid(), + "a different author in the same window is a different tuple: {:?}", + result.errors + ); + + insert_report( + &drive, + &contract, + WINDOW_MS + 9 * 3_600_000, + "bob", + platform_version, + ); + } + + /// Update semantics. `$createdAt` is immutable — that is exactly why a + /// unique time-range index is allowed to bucket it — so on the + /// `ChangedDocument` path the bucket component of the tuple never moves and + /// `allow_original` keeps its ordinary meaning: a document may keep the + /// slot it already holds, but may not move into one another document holds. + #[test] + fn changed_document_keeps_its_own_slot_but_cannot_take_another_documents_slot() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); + + let alice_created_at = WINDOW_MS + 3_600_000; + let alice_id = insert_report( + &drive, + &contract, + alice_created_at, + "alice", + platform_version, + ); + // A second document in the SAME window under a different author. + insert_report( + &drive, + &contract, + WINDOW_MS + 9 * 3_600_000, + "bob", + platform_version, + ); + + // Alice's document changing its author to "bob" walks into the slot + // bob's document already holds, in the same (unchanged) bucket. + let changed_author: BTreeSet = BTreeSet::from(["author".to_string()]); + let result = check_uniqueness( + &drive, + &contract, + alice_id, + alice_created_at, + "bob", + UniquenessOfDataRequestUpdateType::ChangedDocument { + changed_owner_id: false, + changed_updated_at: false, + changed_transferred_at: false, + changed_updated_at_block_height: false, + changed_transferred_at_block_height: false, + changed_updated_at_core_block_height: false, + changed_transferred_at_core_block_height: false, + changed_data_values: Cow::Borrowed(&changed_author), + }, + platform_version, + ); + assert_duplicate(&result, "moving onto another document's tuple"); + + // The same document re-submitted with its own tuple finds itself in + // the bucket and is allowed to stay (`allow_original`). + let unchanged: BTreeSet = BTreeSet::new(); + let result = check_uniqueness( + &drive, + &contract, + alice_id, + alice_created_at, + "alice", + UniquenessOfDataRequestUpdateType::ChangedDocument { + changed_owner_id: false, + changed_updated_at: false, + changed_transferred_at: false, + changed_updated_at_block_height: false, + changed_transferred_at_block_height: false, + changed_updated_at_core_block_height: false, + changed_transferred_at_core_block_height: false, + changed_data_values: Cow::Borrowed(&unchanged), + }, + platform_version, + ); + assert!( + result.is_valid(), + "a document must be allowed to keep the slot it already holds: {:?}", + result.errors + ); + } + + /// A `$createdAt` inside the epoch sliver before the grid's phase anchor + /// produces no index entries at all (the insert walker writes none), so + /// such a document cannot collide with anything: the probe must skip this + /// index entirely rather than invent a bucket for a timestamp that + /// belongs to none. No real timestamp reaches the sliver — this pins the + /// defensive rule all three walkers and the probe share. + #[test] + fn epoch_sliver_candidate_skips_the_time_range_index_check() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_with_phase(Some(PHASE_SECONDS), platform_version); + + // A stored report in the phased grid's first window `[12h, 36h)`. + insert_report( + &drive, + &contract, + PHASE_MS + 3_600_000, + "alice", + platform_version, + ); + + // Same author, timestamp one millisecond before the phase anchor: it + // belongs to no window, so there is nothing for it to duplicate. + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + PHASE_MS - 1, + "alice", + UniquenessOfDataRequestUpdateType::NewDocument, + platform_version, + ); + assert!( + result.is_valid(), + "an epoch-sliver document is not indexed, so it cannot collide: {:?}", + result.errors + ); + + // Contrast: inside the first window the same author does collide, so + // the skip above is the sliver talking and not a probe that silently + // stopped working on this contract. + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + PHASE_MS + 9 * 3_600_000, + "alice", + UniquenessOfDataRequestUpdateType::NewDocument, + platform_version, + ); + assert_duplicate(&result, "inside the first window"); + } +} diff --git a/packages/rs-drive/src/drive/document/insert/add_document_for_contract/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_for_contract/mod.rs index df06944d777..28310f15ada 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_for_contract/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_for_contract/mod.rs @@ -62,3 +62,6 @@ impl Drive { } } } + +#[cfg(test)] +mod time_range_index_e2e_tests; diff --git a/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs b/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs new file mode 100644 index 00000000000..d26fee42864 --- /dev/null +++ b/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs @@ -0,0 +1,1635 @@ +//! End-to-end coverage for time-range index fan-out: a single document is +//! indexed under every overlapping range bucket its `$createdAt` falls +//! into, those buckets are queryable by exact bucket start, and deletion +//! removes every entry. +//! +//! Also covers the other half of that contract: a bucket-start equality +//! only means "bucket" when it came from `IN_TIME_RANGE` resolution, so +//! index selection is pinned by +//! [`DriveDocumentQuery::resolved_time_ranges`] rather than left to +//! whichever index happens to cover the fields. +use crate::config::DriveConfig; +use crate::drive::Drive; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::{ + resolve_time_range_bucket_clause, DriveDocumentQuery, ResolvedTimeRange, TimeRangeGridSpec, + TimeRangeSelector, +}; +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::DocumentTypeRef; +use dpp::data_contract::DataContractFactory; +use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; +use dpp::document::{Document, DocumentV0, DocumentV0Getters, DocumentV0Setters}; +use dpp::platform_value::{platform_value, Identifier, Value}; +use dpp::prelude::DataContract; +use dpp::version::PlatformVersion; +use std::collections::BTreeMap; + +/// One hour in each of the two units these tests deal in: `*_SECONDS` +/// declares a contract's window, `*_MS` is a document timestamp, a bucket +/// start or an index key. Scaling the wrong one silently shifts the +/// buckets by a factor of a thousand, so they are kept apart by name. +const HOUR_SECONDS: u64 = 3_600; +const HOUR_MS: u64 = 3_600_000; + +/// Deterministic 32-byte fixture identifier derived from the document's +/// own fixture inputs. Identifiers here are plumbing, not test inputs: +/// fixed bytes keep a failing GroveDB fixture reproducible run-to-run and +/// avoid an OS-entropy dependency (and its unwrap) in +/// consensus-sensitive tests. `marker` separates namespaces (document id +/// vs owner) and same-timestamp siblings. +fn fixture_bytes(marker: u8, created_at: u64, tag: &str) -> [u8; 32] { + let mut bytes = [0u8; 32]; + bytes[0] = marker; + bytes[1..9].copy_from_slice(&created_at.to_be_bytes()); + for (i, byte) in tag.bytes().take(23).enumerate() { + bytes[9 + i] = byte; + } + bytes +} + +/// A latest-protocol `post` document type with a `(timeRange($createdAt, range=6h, +/// step=2h), hashtag)` countable index — i.e. trending hashtags over a +/// 6-hour window refreshed every 2 hours (overlap factor 3). +fn build_trending_contract() -> DataContract { + let factory = + DataContractFactory::new(PlatformVersion::latest().protocol_version).expect("factory"); + let index_map = vec![ + ( + Value::Text("name".to_string()), + Value::Text("trending".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"$createdAt": "asc"}), + platform_value!({"hashtag": "asc"}), + ]), + ), + ( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + ( + Value::Text("range".to_string()), + Value::U64(6 * HOUR_SECONDS), + ), + ( + Value::Text("step".to_string()), + Value::U64(2 * HOUR_SECONDS), + ), + ]), + ), + ( + Value::Text("countable".to_string()), + Value::Text("countable".to_string()), + ), + ]; + + let document_schema = platform_value!({ + "type": "object", + "properties": { + "hashtag": {"type": "string", "maxLength": 63, "position": 0}, + }, + "required": ["hashtag", "$createdAt"], + "indices": Value::Array(vec![Value::Map(index_map)]), + "additionalProperties": false, + }); + let schemas = platform_value!({ "post": document_schema }); + let owner_id = Identifier::from([201u8; 32]); + factory + .create_with_value_config(owner_id, 0, schemas, None, None) + .expect("create contract") + .data_contract_owned() +} + +/// Number of documents the `trending` index returns for an exact +/// `$createdAt == bucket` lookup. +/// +/// The equality is what `IN_TIME_RANGE` resolution produces, so the query +/// is marked as such: without that provenance index selection refuses to +/// bind a bare `$createdAt` equality to a bucketed index, exactly as it +/// refuses a client-written one. +fn count_in_bucket( + drive: &Drive, + contract: &DataContract, + bucket: u64, + platform_version: &PlatformVersion, +) -> usize { + let document_type = contract.document_type_for_name("post").expect("post"); + let query = build_created_at_query( + contract, + document_type, + bucket, + None, + created_at_resolution(document_type), + ); + query + .execute_raw_results_no_proof(drive, None, None, platform_version) + .expect("query") + .0 + .len() +} + +/// The provenance a real `IN_TIME_RANGE` resolution against this document +/// type's (single) `$createdAt` grid would have produced: the field plus +/// the exact transform, which is what pins index selection to the grid. +fn created_at_resolution(document_type: DocumentTypeRef) -> Vec { + let transform = document_type + .indexes() + .values() + .find_map(|index| index.time_range.clone()) + .expect("the fixture declares a time-range index"); + vec![ResolvedTimeRange { transform }] +} + +/// A `$createdAt == created_at` query, optionally ANDed with +/// `hashtag == `, carrying `resolved_time_ranges` verbatim +/// so tests can drive both the resolved and the raw (empty) provenance. +fn build_created_at_query<'a>( + contract: &'a DataContract, + document_type: DocumentTypeRef<'a>, + created_at: u64, + hashtag: Option<&str>, + resolved_time_ranges: Vec, +) -> DriveDocumentQuery<'a> { + let mut clauses = vec![Value::Array(vec![ + Value::Text("$createdAt".to_string()), + Value::Text("==".to_string()), + Value::U64(created_at), + ])]; + if let Some(hashtag) = hashtag { + clauses.push(Value::Array(vec![ + Value::Text("hashtag".to_string()), + Value::Text("==".to_string()), + Value::Text(hashtag.to_string()), + ])); + } + let query_value = Value::Map(vec![( + Value::Text("where".to_string()), + Value::Array(clauses), + )]); + let mut query = DriveDocumentQuery::from_value( + query_value, + contract, + document_type, + &DriveConfig::default(), + PlatformVersion::latest(), + ) + .expect("build query"); + query.resolved_time_ranges = resolved_time_ranges; + query +} + +#[test] +fn time_range_insert_fans_out_to_overlapping_buckets_and_delete_removes_them() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_trending_contract(); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + let document_type = contract.document_type_for_name("post").expect("post"); + let transform = document_type + .indexes() + .get("trending") + .expect("trending index") + .time_range + .clone() + .expect("time range transform"); + assert_eq!(transform.overlap_factor(), 3); + + // A document created at 7h+ falls into the ranges starting at 6h, 4h, 2h. + let created_at = 7 * HOUR_MS + 123_456; + let expected_buckets = transform.containing_buckets(created_at); + assert_eq!( + expected_buckets, + vec![6 * HOUR_MS, 4 * HOUR_MS, 2 * HOUR_MS] + ); + + let owner_bytes = fixture_bytes(1, created_at, "ibiza"); + let document = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(2, created_at, "ibiza")), + owner_id: Identifier::from(owner_bytes), + properties: BTreeMap::from([("hashtag".to_string(), Value::Text("ibiza".to_string()))]), + created_at: Some(created_at), + ..Default::default() + }); + let document_id = document.id(); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(owner_bytes), + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add document"); + + // The document is queryable under each of its 3 overlapping buckets. + for bucket in &expected_buckets { + assert_eq!( + count_in_bucket(&drive, &contract, *bucket, platform_version), + 1, + "document should be indexed under bucket {bucket}" + ); + } + // It is NOT stored under the raw timestamp (only under bucket starts)… + assert_eq!( + count_in_bucket(&drive, &contract, created_at, platform_version), + 0, + "document must be indexed under bucket starts, not the raw timestamp" + ); + // …nor under a range that does not contain it. + assert_eq!( + count_in_bucket(&drive, &contract, 0, platform_version), + 0, + "an unrelated bucket must be empty" + ); + + // Deleting the document removes every bucket entry. + drive + .delete_document_for_contract( + document_id, + &contract, + "post", + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("delete document"); + + for bucket in &expected_buckets { + assert_eq!( + count_in_bucket(&drive, &contract, *bucket, platform_version), + 0, + "bucket {bucket} should be empty after deletion" + ); + } +} + +/// Update-path set-diff coverage: moving a timestamp between bucket sets +/// must delete the stale entries and insert the new ones, and a +/// sub-property change at an unchanged timestamp must reinsert under the +/// new suffix without duplicating entries. (Null transitions are +/// unreachable through a valid contract: the transform's system-timestamp +/// source must be a required field, so documents always carry it; the +/// walkers' null-entry handling is defense-in-depth covered by +/// `TimeRangeTransform::entry_keys_for_raw`'s unit tests.) +#[test] +fn time_range_update_moves_between_buckets_and_suffix_changes() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_trending_contract(); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + let document_type = contract.document_type_for_name("post").expect("post"); + let transform = document_type + .indexes() + .get("trending") + .expect("trending index") + .time_range + .clone() + .expect("time range transform"); + + let first_created_at = 7 * HOUR_MS + 123_456; + let first_buckets = transform.containing_buckets(first_created_at); + assert_eq!(first_buckets, vec![6 * HOUR_MS, 4 * HOUR_MS, 2 * HOUR_MS]); + let second_created_at = 13 * HOUR_MS + 42; + let second_buckets = transform.containing_buckets(second_created_at); + assert_eq!( + second_buckets, + vec![12 * HOUR_MS, 10 * HOUR_MS, 8 * HOUR_MS] + ); + + let owner_bytes = fixture_bytes(1, first_created_at, "ibiza"); + let mut document = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(2, first_created_at, "ibiza")), + owner_id: Identifier::from(owner_bytes), + properties: BTreeMap::from([("hashtag".to_string(), Value::Text("ibiza".to_string()))]), + created_at: Some(first_created_at), + revision: Some(1), + ..Default::default() + }); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(owner_bytes), + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add document"); + + let update = |document: &Document, step: &str| { + drive + .update_document_for_contract( + document, + &contract, + document_type, + Some(owner_bytes), + BlockInfo::default(), + true, + None, + None, + platform_version, + None, + ) + .unwrap_or_else(|e| panic!("update document ({step}): {e:?}")); + }; + + // Move the timestamp to a disjoint bucket set: the stale entries must + // be deleted and the new ones inserted. + document.set_created_at(Some(second_created_at)); + document.set_revision(Some(2)); + update(&document, "move buckets"); + for bucket in &first_buckets { + assert_eq!( + count_in_bucket(&drive, &contract, *bucket, platform_version), + 0, + "old bucket {bucket} should be empty after the timestamp moved" + ); + } + for bucket in &second_buckets { + assert_eq!( + count_in_bucket(&drive, &contract, *bucket, platform_version), + 1, + "new bucket {bucket} should hold the document after the timestamp moved" + ); + } + + // A sub-property change at an unchanged timestamp reinserts under the + // new suffix without duplicating bucket entries. + document.set("hashtag", Value::Text("mykonos".to_string())); + document.set_revision(Some(3)); + update(&document, "suffix change"); + for bucket in &second_buckets { + assert_eq!( + count_in_bucket(&drive, &contract, *bucket, platform_version), + 1, + "bucket {bucket} should still hold exactly one entry after a suffix change" + ); + } + + // Deleting the document (now holding bucket entries created by the + // update path) removes every entry. + drive + .delete_document_for_contract( + document.id(), + &contract, + "post", + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("delete document"); + for bucket in &second_buckets { + assert_eq!( + count_in_bucket(&drive, &contract, *bucket, platform_version), + 0, + "bucket {bucket} should be empty after deletion" + ); + } +} + +/// The `post` type of [`build_trending_contract`] plus two plain indexes +/// over the same fields, storing raw timestamps: +/// +/// - `byHashtag` — `(hashtag, $createdAt)`. Covers exactly the fields the +/// bucketed `trending` index covers, and sorts before it, so a search +/// that only scores field coverage — ties broken by the index map's name +/// order — always prefers it. Which of the two is correct depends +/// entirely on where the `$createdAt` value came from. +/// - `byHashtagAndAuthor` — `(hashtag, $createdAt, author)`. The only +/// index that can also serve an ordering by `author`, so it is what an +/// unpinned search falls back to for a time-range query that orders by +/// a property the bucketed index does not carry. +/// +/// Both plain indexes start with `hashtag` rather than `$createdAt` so +/// their `$createdAt` entries hold raw timestamps at a non-leading +/// position — the competing-coverage shape these tests need. (A plain +/// index MAY lead with a bucketed field: grids fork into sibling +/// subtrees via grid-qualified level keys, so no cross-index agreement +/// rule exists; safety comes from provenance-pinned index selection.) +fn build_competing_index_trending_contract() -> DataContract { + let factory = + DataContractFactory::new(PlatformVersion::latest().protocol_version).expect("factory"); + let trending_index = vec![ + ( + Value::Text("name".to_string()), + Value::Text("trending".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"$createdAt": "asc"}), + platform_value!({"hashtag": "asc"}), + ]), + ), + ( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + ( + Value::Text("range".to_string()), + Value::U64(6 * HOUR_SECONDS), + ), + ( + Value::Text("step".to_string()), + Value::U64(2 * HOUR_SECONDS), + ), + ]), + ), + ( + Value::Text("countable".to_string()), + Value::Text("countable".to_string()), + ), + ]; + let by_hashtag_index = vec![ + ( + Value::Text("name".to_string()), + Value::Text("byHashtag".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"hashtag": "asc"}), + platform_value!({"$createdAt": "asc"}), + ]), + ), + ]; + let by_hashtag_and_author_index = vec![ + ( + Value::Text("name".to_string()), + Value::Text("byHashtagAndAuthor".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"hashtag": "asc"}), + platform_value!({"$createdAt": "asc"}), + platform_value!({"author": "asc"}), + ]), + ), + ]; + + let document_schema = platform_value!({ + "type": "object", + "properties": { + "hashtag": {"type": "string", "maxLength": 63, "position": 0}, + "author": {"type": "string", "maxLength": 63, "position": 1}, + }, + "required": ["hashtag", "$createdAt"], + "indices": Value::Array(vec![ + Value::Map(by_hashtag_index), + Value::Map(by_hashtag_and_author_index), + Value::Map(trending_index), + ]), + "additionalProperties": false, + }); + let schemas = platform_value!({ "post": document_schema }); + let owner_id = Identifier::from([201u8; 32]); + factory + .create_with_value_config(owner_id, 0, schemas, None, None) + .expect("create contract") + .data_contract_owned() +} + +/// Stores one `post` with the given timestamp and hashtag. +fn insert_post( + drive: &Drive, + contract: &DataContract, + created_at: u64, + hashtag: &str, + platform_version: &PlatformVersion, +) { + let document_type = contract.document_type_for_name("post").expect("post"); + let owner_bytes = fixture_bytes(1, created_at, hashtag); + let document = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(2, created_at, hashtag)), + owner_id: Identifier::from(owner_bytes), + properties: BTreeMap::from([("hashtag".to_string(), Value::Text(hashtag.to_string()))]), + created_at: Some(created_at), + ..Default::default() + }); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(owner_bytes), + }, + contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add document"); +} + +/// With two indexes covering the same fields, the value's provenance — +/// not the index map's name order — decides which index serves the query: +/// a resolved bucket start goes to the bucketed index, a raw timestamp to +/// the plain one. Getting this wrong returns a validly-proven empty result +/// in either direction, so both halves are asserted. +#[test] +fn resolved_time_range_equality_pins_the_bucketed_index_while_a_raw_one_uses_the_plain_index() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_competing_index_trending_contract(); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + let document_type = contract.document_type_for_name("post").expect("post"); + assert_eq!( + document_type.indexes().keys().next().map(String::as_str), + Some("byHashtag"), + "the plain index must sort first for this to reproduce the tie-break the \ + pinning fixes" + ); + + let created_at = 7 * HOUR_MS + 123_456; + let bucket = 6 * HOUR_MS; + insert_post(&drive, &contract, created_at, "ibiza", platform_version); + // A second post in the same bucket under a different hashtag: the + // hashtag equality must still narrow the result to one document. + insert_post(&drive, &contract, created_at, "mykonos", platform_version); + + let resolved = build_created_at_query( + &contract, + document_type, + bucket, + Some("ibiza"), + created_at_resolution(document_type), + ); + assert_eq!( + resolved + .find_best_index(platform_version) + .expect("the bucketed index covers the resolved query") + .name, + "trending" + ); + assert_eq!( + resolved + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("resolved query executes") + .0 + .len(), + 1, + "the resolved bucket equality must find the document stored under that bucket" + ); + + let raw = build_created_at_query(&contract, document_type, created_at, Some("ibiza"), vec![]); + assert_eq!( + raw.find_best_index(platform_version) + .expect("the plain index covers the raw query") + .name, + "byHashtag", + "a raw `$createdAt` equality must never bind to bucket keys" + ); + assert_eq!( + raw.execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("raw query executes") + .0 + .len(), + 1, + "the plain index stores raw timestamps, so the raw equality finds the document" + ); + + // The converse of the raw case: a bucket start is not a timestamp any + // document carries, so the plain index legitimately matches nothing. + let raw_on_bucket = + build_created_at_query(&contract, document_type, bucket, Some("ibiza"), vec![]); + assert_eq!( + raw_on_bucket + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("raw query executes") + .0 + .len(), + 0 + ); +} + +/// A contract whose only index buckets `$createdAt` alone — the shape +/// where a resolved bucket equality is the query's *last* clause with no +/// left-over index properties, so cursor pagination has nothing below the +/// transformed level except the document-id terminal. +/// +/// `unique` flips the index's uniqueness: the grid already satisfies the +/// unique shape (`range == step` on `$createdAt`), and the unique terminal +/// layout stores the reference AT the value tree rather than under an id +/// terminal — the other arm the in-bucket cursor rule must handle. +fn build_single_property_trending_contract(unique: bool) -> DataContract { + let factory = + DataContractFactory::new(PlatformVersion::latest().protocol_version).expect("factory"); + let index_map = vec![ + ( + Value::Text("name".to_string()), + Value::Text("byBucket".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![platform_value!({"$createdAt": "asc"})]), + ), + ( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + ( + Value::Text("range".to_string()), + Value::U64(6 * HOUR_SECONDS), + ), + ( + Value::Text("step".to_string()), + Value::U64(6 * HOUR_SECONDS), + ), + ]), + ), + (Value::Text("unique".to_string()), Value::Bool(unique)), + ]; + + let document_schema = platform_value!({ + "type": "object", + "properties": { + "hashtag": {"type": "string", "maxLength": 63, "position": 0}, + }, + "required": ["hashtag", "$createdAt"], + "indices": Value::Array(vec![Value::Map(index_map)]), + "additionalProperties": false, + }); + let schemas = platform_value!({ "post": document_schema }); + let owner_id = Identifier::from([202u8; 32]); + factory + .create_with_value_config(owner_id, 0, schemas, None, None) + .expect("create contract") + .data_contract_owned() +} + +/// The ids of raw query results, in returned order. +fn result_ids( + results: &[Vec], + document_type: DocumentTypeRef, + platform_version: &PlatformVersion, +) -> Vec<[u8; 32]> { + results + .iter() + .map(|serialized| { + Document::from_bytes(serialized, document_type, platform_version) + .expect("deserialize result") + .id() + .to_buffer() + }) + .collect() +} + +/// The transformed level stores bucket starts, so a cursor document's raw +/// timestamp must never be compared against this level's keys: an +/// included cursor created *inside* the selected bucket (07:10 in a +/// bucket starting at 06:00) orders after the bucket-start key, and +/// applying it at the transformed level suppresses the only key — a +/// validly-proven empty page while later document ids still exist in the +/// bucket. The cursor belongs to the document-id terminal instead. +#[test] +fn included_cursor_inside_the_bucket_continues_a_single_property_time_range_query() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_single_property_trending_contract(false); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + let document_type = contract.document_type_for_name("post").expect("post"); + + // Three posts inside the [06:00, 12:00) bucket, none at its start. + let bucket = 6 * HOUR_MS; + let timestamps: [(u64, &str); 3] = [ + (6 * HOUR_MS + 600_000, "early"), + (7 * HOUR_MS + 123_456, "mid"), + (8 * HOUR_MS, "late"), + ]; + for (created_at, hashtag) in timestamps { + insert_post(&drive, &contract, created_at, hashtag, platform_version); + } + + // The document-id terminal walks ids ascending; the cursor is the + // mid-bucket post, so the expected continuation is every id at or + // after it in that order. + let mut ids: Vec<[u8; 32]> = timestamps + .iter() + .map(|(created_at, hashtag)| fixture_bytes(2, *created_at, hashtag)) + .collect(); + ids.sort(); + let cursor_id = fixture_bytes(2, 7 * HOUR_MS + 123_456, "mid"); + let cursor_position = ids.iter().position(|id| *id == cursor_id).expect("cursor"); + + let mut query = build_created_at_query( + &contract, + document_type, + bucket, + None, + created_at_resolution(document_type), + ); + query.start_at = Some(cursor_id); + query.start_at_included = true; + + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("query with an in-bucket cursor"); + assert_eq!( + result_ids(&results, document_type, platform_version), + &ids[cursor_position..], + "an included in-bucket cursor must return itself and every later id \ + in the bucket, in id order" + ); + + query.start_at_included = false; + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("query with an excluded in-bucket cursor"); + assert_eq!( + result_ids(&results, document_type, platform_version), + &ids[cursor_position + 1..], + "an excluded in-bucket cursor must return every id after it in the \ + bucket, in id order" + ); +} + +/// The unique arm of the in-bucket cursor rule: a unique single-property +/// time-range index (`range == step` on `$createdAt`) stores the bucket's +/// sole reference AT the value tree, with no document-id terminal below +/// it. An included cursor must retain the bucket's one document; an +/// excluded cursor must produce an empty page — never an error, and never +/// the full page again. +#[test] +fn in_bucket_cursor_on_a_unique_single_property_time_range_index() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_single_property_trending_contract(true); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + let document_type = contract.document_type_for_name("post").expect("post"); + + // One document in the [06:00, 12:00) bucket — the most a unique + // single-property grid admits. + let created_at = 7 * HOUR_MS + 123_456; + insert_post(&drive, &contract, created_at, "solo", platform_version); + let document_id = fixture_bytes(2, created_at, "solo"); + let bucket = 6 * HOUR_MS; + + let mut query = build_created_at_query( + &contract, + document_type, + bucket, + None, + created_at_resolution(document_type), + ); + query.start_at = Some(document_id); + query.start_at_included = true; + + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("included cursor on the unique layout"); + assert_eq!( + result_ids(&results, document_type, platform_version), + vec![document_id], + "an included cursor must retain the bucket's sole document" + ); + + query.start_at_included = false; + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("excluded cursor on the unique layout"); + assert!( + results.is_empty(), + "an excluded cursor must empty the page — the bucket holds exactly the cursor" + ); +} + +/// One index can bucket only one field (a transform's source must be its +/// index's first property), so a query resolving two time ranges has no +/// servable shape and is refused rather than routed to whichever index +/// happens to cover the fields. +#[test] +fn two_resolved_time_ranges_are_rejected() { + let contract = build_competing_index_trending_contract(); + let document_type = contract.document_type_for_name("post").expect("post"); + let query = build_created_at_query(&contract, document_type, 6 * HOUR_MS, Some("ibiza"), { + let mut resolutions = created_at_resolution(document_type); + let mut second = resolutions[0].clone(); + second.transform.source = "hashtag".to_string(); + resolutions.push(second); + resolutions + }); + let error = query + .find_best_index(PlatformVersion::latest()) + .expect_err("two resolved time-range fields cannot be served"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected an Unsupported rejection, got {error:?}" + ); +} + +/// Ordering by a property the bucketed index does not carry must fail +/// loudly. `byHashtagAndAuthor` covers the same where fields *and* the +/// ordering, so an unpinned search would take it and match the bucket +/// start against raw timestamps — a validly-proven empty result. +#[test] +fn resolved_time_range_query_ordering_off_the_bucketed_index_errors_rather_than_falling_back() { + let contract = build_competing_index_trending_contract(); + let document_type = contract.document_type_for_name("post").expect("post"); + let query_value = Value::Map(vec![ + ( + Value::Text("where".to_string()), + Value::Array(vec![ + Value::Array(vec![ + Value::Text("hashtag".to_string()), + Value::Text("==".to_string()), + Value::Text("ibiza".to_string()), + ]), + Value::Array(vec![ + Value::Text("$createdAt".to_string()), + Value::Text("==".to_string()), + Value::U64(6 * HOUR_MS), + ]), + ]), + ), + ( + Value::Text("orderBy".to_string()), + Value::Array(vec![Value::Array(vec![ + Value::Text("author".to_string()), + Value::Text("asc".to_string()), + ])]), + ), + ]); + let mut query = DriveDocumentQuery::from_value( + query_value, + &contract, + document_type, + &DriveConfig::default(), + PlatformVersion::latest(), + ) + .expect("build query"); + // Sanity: without the provenance this query has a covering index, so + // the rejection below is the pinning talking and not a query that + // nothing could serve. + assert_eq!( + query + .find_best_index(PlatformVersion::latest()) + .expect("a plain index covers the where fields and the ordering") + .name, + "byHashtagAndAuthor" + ); + + query.resolved_time_ranges = created_at_resolution(document_type); + let error = query + .find_best_index(PlatformVersion::latest()) + .expect_err("no bucketed index covers the ordering"); + assert!( + matches!( + error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(_)) + ), + "expected a no-covering-index rejection, got {error:?}" + ); +} + +const DAY_SECONDS: u64 = 24 * HOUR_SECONDS; +const DAY_MS: u64 = 24 * HOUR_MS; + +/// A `report` document type with a **unique** +/// `(timeRange($createdAt, range = step = 1 day), author)` index — one +/// report per author per calendar day. +/// +/// `range == step` makes the windows a partition (overlap factor 1), which +/// is what lets uniqueness mean anything here, and `$createdAt` is +/// immutable so a document's bucket never moves. Both index properties are +/// required, so the terminator always takes the unique layout (the +/// reference stored AT `[0]`, with no per-document subtree). +fn build_unique_daily_report_contract() -> DataContract { + let factory = + DataContractFactory::new(PlatformVersion::latest().protocol_version).expect("factory"); + let index_map = vec![ + ( + Value::Text("name".to_string()), + Value::Text("dailyReport".to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"$createdAt": "asc"}), + platform_value!({"author": "asc"}), + ]), + ), + (Value::Text("unique".to_string()), Value::Bool(true)), + ( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + (Value::Text("range".to_string()), Value::U64(DAY_SECONDS)), + (Value::Text("step".to_string()), Value::U64(DAY_SECONDS)), + ]), + ), + ]; + + let document_schema = platform_value!({ + "type": "object", + "properties": { + "author": {"type": "string", "maxLength": 63, "position": 0}, + }, + "required": ["author", "$createdAt"], + "indices": Value::Array(vec![Value::Map(index_map)]), + "additionalProperties": false, + }); + let schemas = platform_value!({ "report": document_schema }); + let owner_id = Identifier::from([201u8; 32]); + factory + .create_with_value_config(owner_id, 0, schemas, None, None) + .expect("create contract") + .data_contract_owned() +} + +/// Number of `report`s stored under the exact `(bucket, author)` tuple of +/// the unique index. Carries the `IN_TIME_RANGE` provenance for the same +/// reason [`count_in_bucket`] does. +fn count_reports_for( + drive: &Drive, + contract: &DataContract, + bucket: u64, + author: &str, + platform_version: &PlatformVersion, +) -> usize { + let document_type = contract.document_type_for_name("report").expect("report"); + let query_value = Value::Map(vec![( + Value::Text("where".to_string()), + Value::Array(vec![ + Value::Array(vec![ + Value::Text("$createdAt".to_string()), + Value::Text("==".to_string()), + Value::U64(bucket), + ]), + Value::Array(vec![ + Value::Text("author".to_string()), + Value::Text("==".to_string()), + Value::Text(author.to_string()), + ]), + ]), + )]); + let mut query = DriveDocumentQuery::from_value( + query_value, + contract, + document_type, + &DriveConfig::default(), + PlatformVersion::latest(), + ) + .expect("build query"); + query.resolved_time_ranges = created_at_resolution(document_type); + query + .execute_raw_results_no_proof(drive, None, None, platform_version) + .expect("query") + .0 + .len() +} + +/// A suffix change under a **unique** bucketed index exercises the update +/// walker's unique terminator layout end to end: the old `(bucket, author)` +/// slot must be vacated and the new one occupied. Under the non-unique +/// layout the walker would delete a doc-id key that does not exist and +/// write the reference one level too deep, leaving the old entry in place +/// and the new one unfindable. +#[test] +fn unique_time_range_index_update_moves_the_entry_between_suffixes() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_unique_daily_report_contract(); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + let document_type = contract.document_type_for_name("report").expect("report"); + let index = document_type + .indexes() + .get("dailyReport") + .expect("dailyReport index"); + assert!(index.unique, "the index under test must be unique"); + let transform = index + .time_range + .clone() + .expect("dailyReport buckets $createdAt"); + assert_eq!(transform.overlap_factor(), 1); + + let created_at = 100 * DAY_MS + 3 * HOUR_MS; + let bucket = *transform + .containing_buckets(created_at) + .first() + .expect("a post-origin timestamp has exactly one bucket"); + assert_eq!(bucket, 100 * DAY_MS); + + let owner_bytes = fixture_bytes(1, created_at, "alice"); + let mut document = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(2, created_at, "alice")), + owner_id: Identifier::from(owner_bytes), + properties: BTreeMap::from([("author".to_string(), Value::Text("alice".to_string()))]), + created_at: Some(created_at), + revision: Some(1), + ..Default::default() + }); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(owner_bytes), + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add document"); + + // The insert walker's unique terminator is readable through the + // bucketed index. + assert_eq!( + count_reports_for(&drive, &contract, bucket, "alice", platform_version), + 1, + "the inserted report must be found under its (bucket, author) tuple" + ); + + // Change the suffix. `$createdAt` is untouched — it cannot change — + // so the bucket stays and only the author component of the tuple moves. + document.set("author", Value::Text("bob".to_string())); + document.set_revision(Some(2)); + drive + .update_document_for_contract( + &document, + &contract, + document_type, + Some(owner_bytes), + BlockInfo::default(), + true, + None, + None, + platform_version, + None, + ) + .expect("update document"); + + assert_eq!( + count_reports_for(&drive, &contract, bucket, "alice", platform_version), + 0, + "the old (bucket, author) slot must be vacated by the update" + ); + assert_eq!( + count_reports_for(&drive, &contract, bucket, "bob", platform_version), + 1, + "the new (bucket, author) slot must hold the document after the update" + ); + + // The vacated slot is genuinely free again: a second document may take + // it, which only holds if the update actually removed the reference + // rather than leaving a stale one behind. + let second_owner = fixture_bytes(3, created_at, "alice"); + let second = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(4, created_at, "alice")), + owner_id: Identifier::from(second_owner), + properties: BTreeMap::from([("author".to_string(), Value::Text("alice".to_string()))]), + created_at: Some(created_at + HOUR_MS), + revision: Some(1), + ..Default::default() + }); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &second, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(second_owner), + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add second document into the vacated slot"); + assert_eq!( + count_reports_for(&drive, &contract, bucket, "alice", platform_version), + 1 + ); + + // Deleting the updated document clears its slot too — the delete + // walker and the update walker must agree on where the reference is. + drive + .delete_document_for_contract( + document.id(), + &contract, + "report", + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("delete document"); + assert_eq!( + count_reports_for(&drive, &contract, bucket, "bob", platform_version), + 0, + "the updated document's slot must be empty after deletion" + ); +} + +/// The mirror case: when every index covering the query buckets the +/// field, a raw query has nowhere to go and must be refused instead of +/// silently matching a timestamp against bucket starts. +#[test] +fn raw_query_on_a_doctype_whose_only_covering_index_is_bucketed_errors() { + let contract = build_trending_contract(); + let document_type = contract.document_type_for_name("post").expect("post"); + let query = build_created_at_query( + &contract, + document_type, + 7 * HOUR_MS + 123_456, + None, + vec![], + ); + let error = query + .find_best_index(PlatformVersion::latest()) + .expect_err("a raw equality cannot be served by a bucketed index"); + assert!( + matches!( + error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(_)) + ), + "expected a no-covering-index rejection, got {error:?}" + ); +} +/// The multi-grid contract: one timestamp, two grids, sibling subtrees. +/// A 6h/2h "trending" grid and a 24h/24h "daily" grid both bucket +/// `$createdAt`; each level is keyed by the grid-qualified storage key, +/// so the two coexist — including bucket starts that are numerically +/// identical across grids (every daily start is also a trending start). +fn build_two_grid_contract() -> DataContract { + let factory = + DataContractFactory::new(PlatformVersion::latest().protocol_version).expect("factory"); + let grid_index = |name: &str, range_seconds: u64, step_seconds: u64| { + Value::Map(vec![ + ( + Value::Text("name".to_string()), + Value::Text(name.to_string()), + ), + ( + Value::Text("properties".to_string()), + Value::Array(vec![ + platform_value!({"$createdAt": "asc"}), + platform_value!({"hashtag": "asc"}), + ]), + ), + ( + Value::Text("timeRange".to_string()), + Value::Map(vec![ + ( + Value::Text("on".to_string()), + Value::Text("$createdAt".to_string()), + ), + (Value::Text("range".to_string()), Value::U64(range_seconds)), + (Value::Text("step".to_string()), Value::U64(step_seconds)), + ]), + ), + ( + Value::Text("countable".to_string()), + Value::Text("countable".to_string()), + ), + ]) + }; + let document_schema = platform_value!({ + "type": "object", + "properties": { + "hashtag": {"type": "string", "maxLength": 63, "position": 0}, + }, + "required": ["hashtag", "$createdAt"], + "indices": Value::Array(vec![ + grid_index("trending", 6 * HOUR_SECONDS, 2 * HOUR_SECONDS), + grid_index("daily", 24 * HOUR_SECONDS, 24 * HOUR_SECONDS), + ]), + "additionalProperties": false, + }); + let schemas = platform_value!({ "post": document_schema }); + factory + .create_with_value_config(Identifier::from([202u8; 32]), 0, schemas, None, None) + .expect("a contract may bucket one timestamp with several grids") + .data_contract_owned() +} + +/// The provenance of a resolution against one named grid of a +/// multi-grid document type. +fn grid_resolution(contract: &DataContract, index_name: &str) -> Vec { + let transform = contract + .document_type_for_name("post") + .expect("post") + .indexes() + .get(index_name) + .expect("the fixture declares this index") + .time_range + .clone() + .expect("the index carries a transform"); + vec![ResolvedTimeRange { transform }] +} + +/// Two grids over `$createdAt`: a document fans out into each grid's own +/// subtree, a resolution against one grid reads only that grid's bucket +/// — even when the two grids' bucket starts are the same number — and +/// deletion empties both. The bucket start chosen here (24h) is +/// deliberately a start on BOTH grids: without grid-qualified level keys +/// the two entry sets would interleave in one keyspace and the counts +/// below would be wrong in both directions. +#[test] +fn two_grids_over_one_timestamp_write_and_read_independently() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_two_grid_contract(); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + let document_type = contract.document_type_for_name("post").expect("post"); + + // 25h10m: the daily grid buckets it at 24h; the trending grid at + // [24h, 22h, 20h]. 24h is a bucket start on BOTH grids. + let created_at = 25 * HOUR_MS + 10 * 60_000; + let shared_bucket = 24 * HOUR_MS; + + let trending = grid_resolution(&contract, "trending"); + let daily = grid_resolution(&contract, "daily"); + assert_eq!( + trending[0] + .transform + .containing_buckets(created_at) + .first() + .copied(), + Some(shared_bucket) + ); + assert_eq!( + daily[0].transform.containing_buckets(created_at), + vec![shared_bucket], + "the same numeric start on both grids is the point of this fixture" + ); + + let owner_bytes = fixture_bytes(1, created_at, "ibiza"); + let document = Document::V0(DocumentV0 { + id: Identifier::from(fixture_bytes(2, created_at, "ibiza")), + owner_id: Identifier::from(owner_bytes), + properties: BTreeMap::from([("hashtag".to_string(), Value::Text("ibiza".to_string()))]), + created_at: Some(created_at), + ..Default::default() + }); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + StorageFlags::optional_default_as_cow(), + )), + owner_id: Some(owner_bytes), + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add document"); + + let count_for = |resolutions: &Vec, bucket: u64| -> usize { + let query = build_created_at_query( + &contract, + document_type, + bucket, + Some("ibiza"), + resolutions.clone(), + ); + query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("query") + .0 + .len() + }; + + // Selection pins to the resolved grid's index. + let trending_query = build_created_at_query( + &contract, + document_type, + shared_bucket, + Some("ibiza"), + trending.clone(), + ); + assert_eq!( + trending_query + .find_best_index(platform_version) + .expect("the trending grid's index serves its own resolution") + .name, + "trending" + ); + let daily_query = build_created_at_query( + &contract, + document_type, + shared_bucket, + Some("ibiza"), + daily.clone(), + ); + assert_eq!( + daily_query + .find_best_index(platform_version) + .expect("the daily grid's index serves its own resolution") + .name, + "daily" + ); + + // Each grid's subtree holds the document under the shared start, and + // the trending grid additionally holds it under its two older + // overlapping starts — which the daily grid must NOT see. + assert_eq!(count_for(&trending, shared_bucket), 1); + assert_eq!(count_for(&daily, shared_bucket), 1); + assert_eq!(count_for(&trending, 22 * HOUR_MS), 1); + assert_eq!( + count_for(&daily, 22 * HOUR_MS), + 0, + "22h is a trending fan-out entry only; leaking it into the daily \ + grid would mean the levels share a keyspace again" + ); + + // Deletion empties both grids' subtrees. + drive + .delete_document_for_contract( + document.id(), + &contract, + "post", + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("delete document"); + assert_eq!(count_for(&trending, shared_bucket), 0); + assert_eq!(count_for(&trending, 22 * HOUR_MS), 0); + assert_eq!(count_for(&daily, shared_bucket), 0); +} + +/// Resolution over a multi-grid field: the bare selector is ambiguous +/// and refused; a grid spec picks exactly the named grid; a spec no +/// index declares is refused. This is the query-language half of the +/// storage fork the previous test pins. +#[test] +fn multi_grid_resolution_requires_and_honors_a_grid_spec() { + let contract = build_two_grid_contract(); + let document_type = contract.document_type_for_name("post").expect("post"); + let now_ms = 25 * HOUR_MS; + + let error = resolve_time_range_bucket_clause( + "$createdAt", + TimeRangeSelector::Newest, + None, + document_type, + now_ms, + ) + .expect_err("two grids on the field make the bare selector ambiguous"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected the ambiguity rejection, got {error:?}" + ); + + let (clause, resolution) = resolve_time_range_bucket_clause( + "$createdAt", + TimeRangeSelector::Newest, + Some(TimeRangeGridSpec { + range_seconds: 24 * HOUR_SECONDS, + step_seconds: 24 * HOUR_SECONDS, + phase_seconds: 0, + }), + document_type, + now_ms, + ) + .expect("naming the daily grid resolves against it"); + assert_eq!(clause.value, Value::U64(24 * HOUR_MS)); + assert_eq!(resolution.transform.range_seconds, 24 * HOUR_SECONDS); + + let (clause, resolution) = resolve_time_range_bucket_clause( + "$createdAt", + TimeRangeSelector::Newest, + Some(TimeRangeGridSpec { + range_seconds: 6 * HOUR_SECONDS, + step_seconds: 2 * HOUR_SECONDS, + phase_seconds: 0, + }), + document_type, + now_ms, + ) + .expect("naming the trending grid resolves against it"); + assert_eq!( + clause.value, + Value::U64(24 * HOUR_MS), + "at 25h both grids' newest start is 24h — same number, different \ + subtree, which is exactly why provenance carries the grid" + ); + assert_eq!(resolution.transform.step_seconds, 2 * HOUR_SECONDS); + + let error = resolve_time_range_bucket_clause( + "$createdAt", + TimeRangeSelector::Newest, + Some(TimeRangeGridSpec { + range_seconds: 12 * HOUR_SECONDS, + step_seconds: 12 * HOUR_SECONDS, + phase_seconds: 0, + }), + document_type, + now_ms, + ) + .expect_err("a grid no index declares must be refused"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected the unknown-grid rejection, got {error:?}" + ); +} + +/// The multiple-`In` execution lowering picks its index directly +/// (without `find_best_index`), so it must run the shared +/// resolved-source shape guard itself: a direct caller pairing +/// fabricated provenance with an `In` clause ON the bucketed source +/// would otherwise have its raw `In` values serialized as bucket +/// keys — a validly-proven answer over arbitrary buckets. +#[test] +fn multiple_in_route_refuses_an_in_clause_on_the_bucketed_source() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = build_trending_contract(); + let document_type = contract.document_type_for_name("post").expect("post"); + + let query_value = Value::Map(vec![( + Value::Text("where".to_string()), + Value::Array(vec![ + Value::Array(vec![ + Value::Text("$createdAt".to_string()), + Value::Text("in".to_string()), + Value::Array(vec![Value::U64(2 * HOUR_MS), Value::U64(4 * HOUR_MS)]), + ]), + Value::Array(vec![ + Value::Text("hashtag".to_string()), + Value::Text("in".to_string()), + Value::Array(vec![ + Value::Text("dash".to_string()), + Value::Text("evo".to_string()), + ]), + ]), + ]), + )]); + let mut query = DriveDocumentQuery::from_value( + query_value, + &contract, + document_type, + &DriveConfig::default(), + platform_version, + ) + .expect("two In clauses are a valid protocol-version-14 query shape"); + query.resolved_time_ranges = created_at_resolution(document_type); + + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect_err("an In on the bucketed source must not reach bucket keys"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected the source-shape rejection, got {error:?}" + ); +} 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 f7f465faddc..fde3fc84f34 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 @@ -4,7 +4,9 @@ use crate::util::type_constants::DEFAULT_HASH_SIZE_U8; use crate::util::grove_operations::BatchInsertTreeApplyType; use crate::drive::Drive; -use crate::util::object_size_info::{DocumentAndContractInfo, DocumentInfoV0Methods, PathInfo}; +use crate::util::object_size_info::{ + DocumentAndContractInfo, DocumentInfoV0Methods, DriveKeyInfo, PathInfo, +}; use crate::error::fee::FeeError; use crate::error::Error; @@ -16,7 +18,9 @@ use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::version::PlatformVersion; use crate::drive::document::estimation_costs::estimated_sum_trees_for_value_tree_type::estimated_sum_trees_for_value_tree_type; -use crate::drive::document::index_level_tree_types::index_level_tree_types_with_continuation_demotion; +use crate::drive::document::index_level_tree_types::{ + index_level_tree_types_with_continuation_demotion, time_range_index_keys, +}; use crate::drive::document::paths::contract_document_type_path_vec; use grovedb::batch::KeyInfoPath; use grovedb::EstimatedLayerCount::{ApproximateElements, PotentiallyAtMaxElements}; @@ -115,13 +119,23 @@ impl Drive { let mut index_path: Vec> = contract_document_type_path.clone(); index_path.push(Vec::from(name.as_bytes())); + // The level key is the path segment; the document value is read + // from the *source property*. They coincide except on a + // time-range level, whose key is the property name qualified + // with the grid (`TimeRangeTransform::storage_key`) while the + // timestamp still lives under the bare property name. + let property_name = sub_level + .time_range() + .map(|transform| transform.source.as_str()) + .unwrap_or(name.as_str()); + // with the example of the dashpay contract's first index // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId let document_top_field = document_and_contract_info .owned_document_info .document_info .get_raw_for_document_type( - name, + property_name, document_type, document_and_contract_info.owned_document_info.owner_id, Some((sub_level, event_id)), @@ -129,12 +143,11 @@ impl Drive { )? .unwrap_or_default(); - // The zero will not matter here, because the PathKeyInfo is variable - let path_key_info = document_top_field.clone().add_path::<0>(index_path.clone()); // here we are inserting the value tree (per distinct property value) // under the top-level property-name tree. The top-level property-name // tree itself is created at contract setup, so the apply_type's // `in_tree_type` reflects whichever variant the contract setup used. + // Same for every bucket key when this is a time-range node. let value_apply_type = if estimated_costs_only_with_layer_info.is_none() { BatchInsertTreeApplyType::StatefulBatchInsertTree } else { @@ -146,23 +159,17 @@ impl Drive { .unwrap_or_default(), } }; - self.batch_insert_empty_tree_if_not_exists( - path_key_info.clone(), - value_tree_type, - storage_flags, - value_apply_type, - transaction, - previous_batch_operations, - batch_operations, - drive_version, - )?; if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { let document_top_field_estimated_size = document_and_contract_info .owned_document_info .document_info - .get_estimated_size_for_document_type(name, document_type, platform_version)?; + .get_estimated_size_for_document_type( + property_name, + document_type, + platform_version, + )?; if document_top_field_estimated_size > u8::MAX as u16 { return Err(Error::Fee(FeeError::Overflow( @@ -192,40 +199,85 @@ impl Drive { let any_fields_null = document_top_field.is_empty(); let all_fields_null = document_top_field.is_empty(); - let mut index_path_info = if document_and_contract_info - .owned_document_info - .document_info - .is_document_size() - { - // This is a stateless operation - PathInfo::PathWithSizes(KeyInfoPath::from_known_owned_path(index_path)) - } else { - PathInfo::PathAsVec::<0>(index_path) - }; + // A time-range first-property node expands the document's single + // timestamp into one index entry per overlapping range bucket (the + // bucket *start*, encoded exactly like the timestamp). A normal + // property keeps its single key. The entry-key rule (null keeps + // its single null entry, pre-origin timestamps produce no entries, + // undecodable values keep their raw key) lives in ONE place — + // [`TimeRangeTransform::entry_keys_for_raw`] — shared with the + // delete and update walkers so the three can never disagree. + let index_keys: Vec = time_range_index_keys( + sub_level.time_range(), + document_top_field, + // A validated contract cannot exceed this; the clamp only + // bounds estimation work for unvalidated transforms. The + // `unwrap_or(1)` arm is a protocol version without + // time-range indexes, where no transform can exist. + platform_version + .system_limits + .max_time_range_overlap_factor + .unwrap_or(1), + ); - // we push the actual value of the index path - index_path_info.push(document_top_field)?; - // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId/ - - // Propagate the exact (post-demotion) `value_tree_type` we - // just inserted forward as the recursive level's - // `parent_value_tree_type` so its continuation children pick - // the right zero-contribution op. - self.add_indices_for_index_level_for_contract_operations( - document_and_contract_info, - index_path_info, - sub_level, - any_fields_null, - all_fields_null, - value_tree_type, - previous_batch_operations, - &storage_flags, - estimated_costs_only_with_layer_info, - event_id, - transaction, - batch_operations, - platform_version, - )?; + let bucket_count = index_keys.len(); + for (bucket, index_key) in index_keys.into_iter().enumerate() { + // The zero will not matter here, because the PathKeyInfo is variable + let path_key_info = index_key.clone().add_path::<0>(index_path.clone()); + self.batch_insert_empty_tree_if_not_exists( + path_key_info, + value_tree_type, + storage_flags, + value_apply_type, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )?; + + // The final bucket takes ownership of `index_path`; earlier + // buckets (only a time-range fan-out has more than one) + // clone it. + let own_index_path = if bucket + 1 == bucket_count { + std::mem::take(&mut index_path) + } else { + index_path.clone() + }; + let mut index_path_info = if document_and_contract_info + .owned_document_info + .document_info + .is_document_size() + { + // This is a stateless operation + PathInfo::PathWithSizes(KeyInfoPath::from_known_owned_path(own_index_path)) + } else { + PathInfo::PathAsVec::<0>(own_index_path) + }; + + // we push the actual value of the index path + index_path_info.push(index_key)?; + // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId/ + + // Propagate the exact (post-demotion) `value_tree_type` we + // just inserted forward as the recursive level's + // `parent_value_tree_type` so its continuation children pick + // the right zero-contribution op. + self.add_indices_for_index_level_for_contract_operations( + document_and_contract_info, + index_path_info, + sub_level, + any_fields_null, + all_fields_null, + value_tree_type, + previous_batch_operations, + &storage_flags, + estimated_costs_only_with_layer_info, + event_id, + transaction, + batch_operations, + platform_version, + )?; + } } Ok(()) } 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 c245802038e..45771bede30 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 @@ -1,5 +1,7 @@ use crate::drive::constants::CONTRACT_DOCUMENTS_PATH_HEIGHT; -use crate::drive::document::index_level_tree_types::index_level_tree_types_with_continuation_demotion; +use crate::drive::document::index_level_tree_types::{ + index_level_tree_types_with_continuation_demotion, IndexLevelTreeTypes, +}; use crate::drive::document::{ make_document_reference, make_document_reference_with_sum_item, read_document_sum_contribution, }; @@ -16,7 +18,7 @@ use crate::util::object_size_info::DocumentInfo::DocumentOwnedInfo; use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef, KeySize}; use crate::util::object_size_info::PathKeyElementInfo::PathKeyRefElement; use crate::util::object_size_info::{ - DocumentAndContractInfo, DocumentInfoV0Methods, DriveKeyInfo, PathKeyInfo, + DocumentAndContractInfo, DocumentInfo, DocumentInfoV0Methods, DriveKeyInfo, PathKeyInfo, }; use crate::util::storage_flags::StorageFlags; use dpp::block::block_info::BlockInfo; @@ -33,7 +35,9 @@ use crate::drive::document::paths::{ contract_documents_primary_key_path, }; use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods; -use dpp::data_contract::document_type::IndexCountability; +use dpp::data_contract::document_type::{ + DocumentTypeRef, Index, IndexCountability, IndexLevel, TimeRangeTransform, +}; use dpp::version::PlatformVersion; use grovedb::batch::key_info::KeyInfo; use grovedb::batch::key_info::KeyInfo::KnownKey; @@ -307,7 +311,16 @@ impl Drive { let top_index_property = index.properties.first().ok_or(Error::Drive( DriveError::CorruptedContractIndexes("invalid contract indices".to_string()), ))?; - index_path.push(Vec::from(top_index_property.name.as_bytes())); + // A time-range index's top level is keyed by the property name + // qualified with the grid (`TimeRangeTransform::storage_key`), + // so different grids over one timestamp live in sibling + // subtrees; a plain index keeps the bare name. The same key is + // both the path segment and the `IndexLevel` lookup below. + let top_level_key = match index.time_range.as_ref() { + Some(transform) => transform.storage_key(&top_index_property.name), + None => top_index_property.name.clone(), + }; + index_path.push(Vec::from(top_level_key.as_bytes())); // Mirror the insert path's IndexLevel descent. We // start at the top-level property's `IndexLevel` node — @@ -324,15 +337,16 @@ impl Drive { // there). Using `has_index_with_type()` on the descended // node ensures we pick that upgrade rather than the // currently-iterated index's own per-level flags. - let mut current_index_level = index_structure - .sub_levels() - .get(&top_index_property.name) - .ok_or(Error::Drive(DriveError::CorruptedContractIndexes(format!( - "index structure missing top property '{}' for index '{}' — \ + let mut current_index_level = + index_structure + .sub_levels() + .get(&top_level_key) + .ok_or(Error::Drive(DriveError::CorruptedContractIndexes(format!( + "index structure missing top level '{}' for index '{}' — \ doctype's IndexLevel tree must contain every property of every \ registered index", - top_index_property.name, index.name - ))))?; + top_level_key, index.name + ))))?; // Per-index reference variant. Mirror of the insert path's // dispatch in @@ -357,6 +371,38 @@ impl Drive { document_reference.clone() }; + // Time-range indexes store one entry per overlapping range bucket, + // so they need a set-diff update rather than the single old→new + // value transition below. `current_index_level` is still the + // top-level node and `index_path` is the base + // (…//) at this point. The + // transform is read off the `IndexLevel` node — the same source + // the insert and delete walkers branch on — so all three walkers + // agree even on a document type constructed outside full + // validation. (A level's key embeds its grid, so the node's + // transform is exactly the grid every index sharing this level + // declared.) + if let Some(transform) = current_index_level.time_range() { + self.update_time_range_index_for_contract_operations_v1( + index, + transform, + document, + &old_document_info, + owner_id, + document_type, + &index_path, + current_index_level, + &index_document_reference, + storage_flags, + &mut batch_insertion_cache, + previous_batch_operations, + &mut batch_operations, + transaction, + platform_version, + )?; + continue; + } + // with the example of the dashpay contract's first index // the index path is now something likeDataContracts/ContractID/Documents(1)/$ownerId let document_top_field = document @@ -809,4 +855,403 @@ impl Drive { } Ok(batch_operations) } + + /// Updates the index entries for a single **time-range** index. + /// + /// A time-range index stores one entry per overlapping range bucket the + /// document's timestamp falls into, so an update is a set diff rather than + /// the single old→new transition the normal-index path performs: + /// - entry keys present only in the old timestamp's set are removed, + /// - entry keys present only in the new timestamp's set are inserted, + /// - keys present in both are refreshed, or (when a later index property + /// changed) deleted under the old sub-values and reinserted under the new + /// ones. + /// + /// The entry-key set for a document mirrors the insert walker exactly: a + /// decodable timestamp yields one key per containing bucket (none when the + /// timestamp predates the transform's origin), a null timestamp yields the + /// single ordinary null key, and a non-null value that fails to decode + /// yields its raw key — so null→value, value→null and null→null + /// transitions maintain the same entries insert and delete would. + /// + /// Time-range indexes are validated to be non-contested, but they may be + /// unique when the windows do not overlap (`range == step`) and the source + /// is `$createdAt`, so both terminator layouts occur here and are chosen + /// exactly as the insert and delete walkers choose them + /// (`!unique || any_fields_null`): the non-unique layout stores the + /// reference under `…//[0]/`, the unique layout stores it + /// AT `…//[0]`. The predicate is evaluated separately for the new + /// and the old document, because the layout an entry was *written* under + /// is the layout it must be *deleted* under. + /// + /// (With a `$createdAt` source and overlap factor 1 the bucket set can + /// never change on an update — the timestamp is immutable and yields + /// exactly one bucket — so on a unique index only the suffix-change arms + /// below are reachable. The layout dispatch is nonetheless applied to + /// every arm rather than reasoned away per arm: the arms are shared with + /// non-unique indexes, and a future relaxation of the uniqueness rules + /// must not silently resurrect the wrong layout.) + /// + /// The estimated-cost / document-size update path never reaches this + /// method — it delegates to the insert path, which has its own bucket + /// fan-out. + #[allow(clippy::too_many_arguments)] + fn update_time_range_index_for_contract_operations_v1( + &self, + index: &Index, + transform: &TimeRangeTransform, + document: &Document, + old_document_info: &DocumentInfo, + owner_id: Option<[u8; 32]>, + document_type: DocumentTypeRef, + base_index_path: &[Vec], + top_index_level: &IndexLevel, + index_document_reference: &Element, + storage_flags: Option<&StorageFlags>, + batch_insertion_cache: &mut HashSet>>, + previous_batch_operations: &mut Option<&mut Vec>, + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let drive_version = &platform_version.drive; + + // New/old raw values for the bucketed source property → entry key + // sets, mirroring the insert walker's fan-out (see the doc comment). + let new_raw = document.get_raw_for_document_type( + &transform.source, + document_type, + owner_id, + platform_version, + )?; + let old_raw = match old_document_info.get_raw_for_document_type( + &transform.source, + document_type, + None, // We want to use the old owner id + None, + platform_version, + )? { + Some(Key(k)) => Some(k), + Some(KeyRef(k)) => Some(k.to_vec()), + _ => None, + }; + + // The entry-key rule (null → single null entry, pre-origin → no + // entries, undecodable → raw key, decodable → containing buckets) is + // shared with the insert and delete walkers via + // `TimeRangeTransform::entry_keys_for_raw` — one definition, so the + // three walkers can never disagree. + let new_entry_keys = transform.entry_keys_for_raw(new_raw.as_deref().unwrap_or_default()); + let old_entry_keys = transform.entry_keys_for_raw(old_raw.as_deref().unwrap_or_default()); + + // Terminator-layout inputs, tracked separately for the new and the old + // document and accumulated over the suffix properties in the loop + // below. The insert and delete walkers demote a unique index to the + // non-unique layout as soon as ANY indexed field is null + // (`any_fields_null`, accumulated with OR down the level recursion), so + // an entry's layout is a property of the document that wrote it — the + // delete below must therefore use the OLD document's verdict, not the + // new one's. A null source raw value yields the single empty entry key, + // which is exactly the null case here. + let mut new_any_fields_null = new_raw.as_deref().unwrap_or_default().is_empty(); + let mut old_any_fields_null = old_raw.as_deref().unwrap_or_default().is_empty(); + + // Sub-property suffix (positions 1..) interleaved as [name, value, …] + // for both the new and old document, plus the IndexLevel node at each + // depth with its (pure, hoisted) tree-type derivation — the bucket + // loop below runs up to `overlap_factor` times and must not re-derive + // per iteration. + let mut levels: Vec<(&IndexLevel, IndexLevelTreeTypes)> = Vec::new(); + let mut new_suffix: Vec> = Vec::new(); + let mut old_suffix: Vec> = Vec::new(); + let mut current_level = top_index_level; + for index_property in index.properties.iter().skip(1) { + current_level = + current_level + .sub_levels() + .get(&index_property.name) + .ok_or(Error::Drive(DriveError::CorruptedContractIndexes(format!( + "index structure missing sub_level '{}' under time-range index '{}'", + index_property.name, index.name + ))))?; + levels.push(( + current_level, + index_level_tree_types_with_continuation_demotion(current_level)?, + )); + + let new_val = document + .get_raw_for_document_type( + &index_property.name, + document_type, + owner_id, + platform_version, + )? + .unwrap_or_default(); + let old_val = match old_document_info.get_raw_for_document_type( + &index_property.name, + document_type, + None, + None, + platform_version, + )? { + Some(Key(k)) => k, + Some(KeyRef(k)) => k.to_vec(), + _ => Vec::new(), + }; + new_any_fields_null |= new_val.is_empty(); + old_any_fields_null |= old_val.is_empty(); + + new_suffix.push(index_property.name.as_bytes().to_vec()); + new_suffix.push(new_val); + old_suffix.push(index_property.name.as_bytes().to_vec()); + old_suffix.push(old_val); + } + + // The two terminator layouts, mirroring + // `add_reference_for_index_level_for_contract_operations_v0`'s + // `!index_type.index_type.is_unique() || any_fields_null` dispatch. + let new_terminator_is_unique = index.unique && !new_any_fields_null; + let old_terminator_is_unique = index.unique && !old_any_fields_null; + + let suffix_changed = new_suffix != old_suffix; + let reference_tree_type = + reference_tree_type_for_index(index.countable, &index.summable, index.range_summable); + let top_level_tree_types = + index_level_tree_types_with_continuation_demotion(top_index_level)?; + let top_value_tree_type = top_level_tree_types.value_tree_type; + let doc_id = document.id(); + + let new_set: HashSet<&Vec> = new_entry_keys.iter().collect(); + let old_set: HashSet<&Vec> = old_entry_keys.iter().collect(); + + // Insert/refresh new entries FIRST (see the ordering note on the + // delete loop below): added keys are inserted; common keys are + // refreshed when unchanged or reinserted when the suffix changed. + for entry_key in &new_entry_keys { + if old_set.contains(entry_key) && !suffix_changed { + // Unchanged entry — refresh the stored reference in place + // (its content can still differ via storage flags). An entry + // key can only be in both sets when the source value's + // null-ness matched (a null raw yields the empty key, a + // non-null one an 8-byte bucket start), and the suffix is + // unchanged here, so the old and new layouts coincide and + // either verdict describes the entry on disk. + let mut path: Vec> = base_index_path.to_vec(); + path.push(entry_key.clone()); + for segment in &new_suffix { + path.push(segment.clone()); + } + let (refresh_key, refresh_path) = if new_terminator_is_unique { + // Unique layout: the reference lives AT `[0]`. + (vec![0], path) + } else { + // Non-unique layout: `[0]` is a tree of doc-id-keyed + // references. + path.push(vec![0]); + (doc_id.to_vec(), path) + }; + self.batch_refresh_reference( + refresh_path, + refresh_key, + index_document_reference.clone(), + storage_flags.is_none(), + batch_operations, + drive_version, + )?; + continue; + } + + // Materialize every tree along the entry path — the same + // post-demotion tree types and zero-contribution wrappers the + // insert walkers use — then store the reference. The insertion + // cache prevents duplicate empty-tree operations when several + // indexes produce the same qualified path. Indexes sharing a + // first property need NOT share a transform (grids fork into + // sibling subtrees via their grid-qualified level keys), which + // is exactly why the cache keys on the full qualified path + // rather than the property name. + let mut path: Vec> = base_index_path.to_vec(); + let mut qualified_path = path.clone(); + qualified_path.push(entry_key.clone()); + if !batch_insertion_cache.contains(&qualified_path) { + let inserted = self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKeyRef::<0>((path.clone(), entry_key.as_slice())), + top_value_tree_type, + storage_flags, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )?; + if inserted { + batch_insertion_cache.insert(qualified_path); + } + } + path.push(entry_key.clone()); + + let mut parent_value_tree_type = top_value_tree_type; + for (i, (_level, sub_level_tree_types)) in levels.iter().enumerate() { + let property_name = &new_suffix[i * 2]; + let value = &new_suffix[i * 2 + 1]; + + let mut qualified_path = path.clone(); + qualified_path.push(property_name.clone()); + if !batch_insertion_cache.contains(&qualified_path) { + // Continuation property-name tree: contributes zero on + // every axis its aggregating parent maintains, exactly + // as on the insert path. + 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_index_tree_if_not_exists( + PathKeyInfo::PathKeyRef::<0>((path.clone(), property_name.as_slice())), + property_name_tree_type, + ranked_axes, + storage_flags, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )? + } else { + self.batch_insert_empty_tree_contributing_zero_to_aggregating_parent_if_not_exists( + PathKeyInfo::PathKeyRef::<0>((path.clone(), property_name.as_slice())), + parent_value_tree_type, + property_name_tree_type, + ranked_axes, + storage_flags, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )? + }; + if inserted { + batch_insertion_cache.insert(qualified_path); + } + } + path.push(property_name.clone()); + + let mut qualified_path = path.clone(); + qualified_path.push(value.clone()); + if !batch_insertion_cache.contains(&qualified_path) { + let inserted = self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKeyRef::<0>((path.clone(), value.as_slice())), + sub_level_tree_types.value_tree_type, + storage_flags, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )?; + if inserted { + batch_insertion_cache.insert(qualified_path); + } + } + path.push(value.clone()); + + parent_value_tree_type = sub_level_tree_types.value_tree_type; + } + + if new_terminator_is_unique { + // Unique layout: no per-document subtree — the reference IS + // the element at `[0]`, so the slot must be free. Insertion + // failure means two documents claim the same (bucket, …) + // tuple, which the uniqueness validation should have caught + // before we got here; treat it as index corruption exactly as + // the non-time-range branch does. + let inserted = self.batch_insert_if_not_exists( + PathKeyRefElement::<0>((path, &[0], index_document_reference.clone())), + BatchInsertApplyType::StatefulBatchInsert, + transaction, + batch_operations, + drive_version, + )?; + if !inserted { + return Err(Error::Drive(DriveError::CorruptedContractIndexes( + "index already exists".to_string(), + ))); + } + } else { + // Non-unique layout: materialize the `[0]` reference bucket + // (it carries the index's count/sum aggregates) and store the + // reference under the document id. + self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKeyRef::<0>((path.clone(), &[0])), + reference_tree_type, + storage_flags, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )?; + path.push(vec![0]); + + self.batch_insert( + PathKeyRefElement::<0>(( + path, + doc_id.as_slice(), + index_document_reference.clone(), + )), + batch_operations, + drive_version, + )?; + } + } + // Delete old entries LAST: removed keys, plus common keys whose + // sub-values changed (their old-suffix path no longer matches). The + // ordering is load-bearing: `batch_delete_up_tree_while_empty` folds + // the already-accumulated batch operations into its emptiness walk, + // so with the inserts above in place it stops before deleting a + // shared ancestor tree (bucket / property-name) that this same batch + // re-populates — e.g. on a suffix change at an unchanged timestamp. + // Deletes first would emit a delete of that ancestor alongside + // inserts into it, which grovedb's batch consistency check rejects. + for entry_key in &old_entry_keys { + if new_set.contains(entry_key) && !suffix_changed { + continue; // unchanged entry — already refreshed by the insert loop above + } + let mut key_info_path: Vec = base_index_path + .iter() + .map(|s| KnownKey(s.clone())) + .collect(); + key_info_path.push(KnownKey(entry_key.clone())); + for segment in &old_suffix { + key_info_path.push(KnownKey(segment.clone())); + } + // The entry is removed under the layout the OLD document wrote it + // with: under the unique layout `[0]` IS the reference and is the + // key being deleted, under the non-unique layout `[0]` is the + // enclosing tree and the document id is the key. The stop height + // is unaffected — the emptiness walk still climbs the suffix + // values, the property-name trees and the bucket, and still stops + // at the document-type level, whose per-index property-name trees + // belong to contract registration rather than to any document. + let delete_key: Vec = if old_terminator_is_unique { + vec![0] + } else { + key_info_path.push(KnownKey(vec![0])); + doc_id.to_vec() + }; + self.batch_delete_up_tree_while_empty( + KeyInfoPath::from_vec(key_info_path), + delete_key.as_slice(), + Some(CONTRACT_DOCUMENTS_PATH_HEIGHT), + BatchDeleteUpTreeApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )?; + } + + Ok(()) + } } diff --git a/packages/rs-drive/src/drive/document/update/mod.rs b/packages/rs-drive/src/drive/document/update/mod.rs index 407db2b14b1..5f4addfe4d6 100644 --- a/packages/rs-drive/src/drive/document/update/mod.rs +++ b/packages/rs-drive/src/drive/document/update/mod.rs @@ -3264,6 +3264,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let sum_response = drive .execute_document_sum_request(sum_request, None, platform_version) @@ -3501,6 +3502,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let sum_response = drive .execute_document_sum_request(sum_request, None, platform_version) diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs index e7aada7b4d8..ececc853c9c 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs @@ -65,6 +65,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation @@ -111,6 +112,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Fetch all documents diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs index 95633f0b2e9..57fcf6ab630 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs @@ -84,6 +84,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs index 64e84b84754..0f2e5b017e3 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs @@ -87,6 +87,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // todo: deal with cost of this operation diff --git a/packages/rs-drive/src/query/canonicalize.rs b/packages/rs-drive/src/query/canonicalize.rs new file mode 100644 index 00000000000..f8b57f510c7 --- /dev/null +++ b/packages/rs-drive/src/query/canonicalize.rs @@ -0,0 +1,190 @@ +//! Shared where-clause validation + canonicalization for the aggregate +//! query surfaces (count / sum / average / joint count-and-sum) and +//! their SDK proof verifiers. +//! +//! Lives outside the per-surface dispatcher modules because the shape +//! contract must be identical on every route: the server dispatchers +//! canonicalize before mode detection, and the proof verifiers must run +//! the very same canonicalization before *their* mode detection or a +//! proof the server produced for the canonical shape is rejected +//! client-side (the count dispatcher promises callers "the bounded form +//! and the pre-merged form get equivalent mode detection" — that promise +//! only holds if verifiers canonicalize too). + +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::drive_document_count_query::DriveDocumentCountQuery; +use crate::query::WhereClause; +use dpp::version::PlatformVersion; + +/// Run the system-wide where-clause validator on a structured +/// `Vec` and canonicalize same-field range pairs into +/// their `between*` form. Single source of truth for the aggregate +/// shape contract; called by the count / sum / average / joint +/// dispatchers, the legacy CBOR-decoded count entry, and the SDK +/// count / sum / average proof verifiers. +/// +/// The validator (`WhereClause::group_clauses`) rejects: +/// - Duplicate `Equal` clauses on the same field +/// (`DuplicateNonGroupableClauseSameField`). +/// - Multiple `In` clauses (`MultipleInClauses`) — rejected here: the +/// shared grammar accepts them for protocol version 14+ document +/// queries, but the aggregate surfaces do not. +/// - Multiple non-groupable range clauses (`MultipleRangeClauses`). +/// - Equality + `In` on the same field, range + equality/In on the +/// same field (`DuplicateNonGroupableClauseSameField` / +/// `InvalidWhereClauseComponents`). +/// +/// Without this validation, downstream +/// [`DriveDocumentCountQuery::find_countable_index_for_where_clauses`] +/// collapses repeated fields into a `BTreeSet` and +/// [`DriveDocumentCountQuery::point_lookup_count_path_query`] +/// resolves each index property with a single `.find(...)` — both +/// of which silently pick the first clause on a duplicated field +/// and return a count for an arbitrarily reduced query rather than +/// rejecting the malformed request. +/// +/// **Exception**: `MultipleRangeClauses` is intentionally tolerated +/// here. The regular-query parser rejects two ranges on different +/// fields wholesale (its callers expect +/// `(equal_clauses, in_clause, range_clause)` triples), but the +/// count-query path accepts the carrier-aggregate shape +/// (`outer_range + inner_ACOR_range` on different fields, e.g. +/// G8). Structural validation for that shape lives in +/// [`DriveDocumentCountQuery::detect_mode`] (which knows about +/// `CountMode::GroupByRange`-with-two-ranges and routes to +/// `DocumentCountMode::RangeAggregateCarrierProof`); replicating +/// it here would be redundant. +/// +/// After validation, [`merge_same_field_range_pairs`] collapses +/// `[field > A, field < B]` (and analogous pairs with `>=` / `<=`) +/// into the canonical `between*` operator that +/// [`DriveDocumentCountQuery::range_clause_to_query_item`] knows +/// how to convert into a single `QueryItem`. The regular-query +/// parser does the same merge before its grouped-triple +/// validation; for aggregate queries we do it explicitly here so +/// callers can pass either the bounded form (e.g. +/// `[brand > A, brand < B]`) or the pre-merged form (e.g. +/// `[brand BetweenExcludeBounds [A, B]]`) and get equivalent +/// mode detection downstream. Without this merge, G8a's natural +/// wire shape (four range clauses, two per field) would slip past +/// the catch-`MultipleRangeClauses` block above and then get +/// rejected by `detect_mode`'s `range_count > 1` structural check. +pub fn validate_and_canonicalize_where_clauses( + clauses: Vec, + platform_version: &PlatformVersion, +) -> Result, Error> { + match WhereClause::group_clauses(&clauses, platform_version) { + // Multiple `In` clauses are a document-query-only shape (protocol + // version 14+); the aggregate surfaces keep rejecting them since + // their mode detection and index pickers assume a single `In`. + Ok((_, _, in_clauses)) if in_clauses.len() > 1 => { + return Err(Error::Query(QuerySyntaxError::MultipleInClauses( + "aggregate queries support at most one in clause", + ))); + } + Ok(_) => {} + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(_))) => {} + Err(e) => return Err(e), + } + merge_same_field_range_pairs(clauses) +} + +/// Collapse `[field > A, field < B]` (and analogous pairs with +/// `>=` / `<=`) into a single `field between* [A, B]` clause per +/// field. Equality / In clauses pass through unchanged. +/// +/// Returns an error if a field has more than two range clauses +/// (structurally meaningless — a third bound would either +/// contradict an existing one or be redundant) or if the pair +/// isn't one lower-bound + one upper-bound (e.g. two `>` on the +/// same field). +fn merge_same_field_range_pairs(clauses: Vec) -> Result, Error> { + use crate::query::conditions::WhereOperator::{ + Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, GreaterThan, + GreaterThanOrEquals, LessThan, LessThanOrEquals, + }; + use std::collections::BTreeMap; + + let mut by_field: BTreeMap> = BTreeMap::new(); + let mut non_range: Vec = Vec::new(); + for wc in clauses { + if DriveDocumentCountQuery::is_range_operator(wc.operator) { + by_field.entry(wc.field.clone()).or_default().push(wc); + } else { + non_range.push(wc); + } + } + let mut result = non_range; + for (field, mut ranges) in by_field { + match ranges.len() { + 0 => {} + 1 => result.push(ranges.remove(0)), + 2 => { + let (mut lower, mut upper): (Option, Option) = + (None, None); + for r in ranges { + match r.operator { + GreaterThan | GreaterThanOrEquals => { + if lower.is_some() { + return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "two lower-bound range clauses on the same field cannot be \ + merged; combine via `between*` or remove the redundant clause", + ))); + } + lower = Some(r); + } + LessThan | LessThanOrEquals => { + if upper.is_some() { + return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "two upper-bound range clauses on the same field cannot be \ + merged; combine via `between*` or remove the redundant clause", + ))); + } + upper = Some(r); + } + _ => { + // The other range operators (Between*, + // StartsWith) are themselves bounded + // already; a second range clause on the + // same field is structurally redundant. + return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "cannot pair a `between*`/`startsWith` range clause with \ + another range on the same field; use the pre-merged form", + ))); + } + } + } + let lower = lower.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "two range clauses on the same field require one lower bound (> or >=) \ + and one upper bound (< or <=)", + )))?; + let upper = upper.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "two range clauses on the same field require one lower bound (> or >=) \ + and one upper bound (< or <=)", + )))?; + let merged_op = match ( + lower.operator == GreaterThanOrEquals, + upper.operator == LessThanOrEquals, + ) { + (true, true) => Between, // [a, b] + (false, false) => BetweenExcludeBounds, // (a, b) + (true, false) => BetweenExcludeRight, // [a, b) + (false, true) => BetweenExcludeLeft, // (a, b] + }; + result.push(WhereClause { + field, + operator: merged_op, + value: dpp::platform_value::Value::Array(vec![lower.value, upper.value]), + }); + } + _ => { + return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "more than two range clauses on the same field are not supported; a \ + bounded range needs exactly one lower bound and one upper bound", + ))); + } + } + } + Ok(result) +} diff --git a/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs index 26bcd39f12e..da9f8de2de6 100644 --- a/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs @@ -55,6 +55,9 @@ use crate::query::drive_document_sum_query::index_picker::{ find_range_summable_index_for_where_clauses, find_summable_index_for_where_clauses, }; use crate::query::drive_document_sum_query::{is_range_operator, DriveDocumentSumQuery}; +use crate::query::{ + validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes, +}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; use dpp::version::PlatformVersion; @@ -72,11 +75,30 @@ impl Drive { /// dispatcher that reads `(count, sum)` together. pub fn execute_document_average_request( &self, - request: DocumentAverageRequest, + mut request: DocumentAverageRequest, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { + // Provenance-vs-shape contract, BEFORE the prove/no-prove split: the + // no-prove path re-checks inside the joint dispatcher, but the prove + // path would otherwise reach its executors unguarded — a direct + // caller marking an `In`/range clause as time-range-resolved could + // have the pickers admit the bucketed index and prove an aggregate + // that counts a document once per overlapping bucket. The guard only + // inspects Equal clauses, so running it before range-pair + // canonicalization is equivalent to running it after. + validate_resolved_time_range_clause_shapes( + &request.where_clauses, + &request.resolved_time_ranges, + )?; if request.prove { + // The no-prove path canonicalizes inside the joint dispatcher; + // run the identical shared step (see + // [`crate::query::canonicalize`]) on the prove path so both + // accept the bounded pair form (`[f > A, f < B]`) as well as + // the pre-merged `between*` form. + request.where_clauses = + validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; return self.execute_document_average_prove(request, transaction, platform_version); } self.execute_document_count_and_sum_request(request, transaction, platform_version) @@ -176,6 +198,7 @@ impl Drive { request.document_type.indexes(), &request.where_clauses, &request.sum_property, + &request.resolved_time_ranges, ) .filter(|idx| idx.range_countable) .ok_or_else(|| { @@ -262,6 +285,7 @@ impl Drive { request.document_type.indexes(), &request.where_clauses, &request.sum_property, + &request.resolved_time_ranges, ) .filter(|idx| idx.range_countable) .ok_or_else(|| { @@ -350,6 +374,7 @@ impl Drive { request.document_type.indexes(), &request.where_clauses, &request.sum_property, + &request.resolved_time_ranges, ) .filter(|idx| idx.countable.is_countable()) .ok_or_else(|| { @@ -394,6 +419,7 @@ impl Drive { #[cfg(all(test, feature = "server"))] mod tests { use super::*; + use crate::query::ResolvedTimeRange; // ── Dispatcher limit-policy regression tests ─────────────────── // @@ -599,6 +625,7 @@ mod tests { limit: None, prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -616,6 +643,7 @@ mod tests { document_type.indexes(), std::slice::from_ref(&color_gt_blue), "amount", + &[], ) .filter(|idx| idx.range_countable) .expect("byColor rangeAverageable index covers `color > blue`"); @@ -693,6 +721,7 @@ mod tests { limit: Some(over_max), prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let err = drive @@ -807,6 +836,7 @@ mod tests { limit: None, prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -884,6 +914,7 @@ mod tests { limit: None, prove: false, drive_config, + resolved_time_ranges: vec![], }; let sum_request = DocumentSumRequest { contract, @@ -895,6 +926,7 @@ mod tests { limit: None, prove: false, drive_config, + resolved_time_ranges: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1009,6 +1041,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let joint_response = drive @@ -1120,6 +1153,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let joint_response = drive @@ -1140,6 +1174,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let sum_request = DocumentSumRequest { contract: &data_contract, @@ -1151,6 +1186,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1254,6 +1290,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let joint_response = drive @@ -1274,6 +1311,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let sum_request = DocumentSumRequest { contract: &data_contract, @@ -1285,6 +1323,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1377,6 +1416,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let joint_response = drive @@ -1509,6 +1549,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let joint_response = drive @@ -1529,6 +1570,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let sum_request = DocumentSumRequest { contract: &data_contract, @@ -1540,6 +1582,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1642,6 +1685,7 @@ mod tests { limit: Some(2), prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -1720,6 +1764,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -1798,6 +1843,7 @@ mod tests { limit: Some(4), prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -1853,6 +1899,7 @@ mod tests { limit: None, prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let err = drive @@ -1915,6 +1962,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let err = drive @@ -1933,6 +1981,67 @@ mod tests { ); } + /// The prove path returns before the joint dispatcher, so the + /// provenance-vs-shape guard must run at the shared entry: without it a + /// direct caller marking an `In` clause as time-range-resolved would + /// reach the prove executors, have the pickers admit a bucketed index, + /// and prove an aggregate that counts a document once per overlapping + /// bucket. + #[test] + fn avg_prove_path_rejects_resolved_time_range_provenance_on_an_in_clause() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let data_contract = build_widget_contract_pcps(); + drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + let document_type = data_contract + .document_type_for_name("widget") + .expect("widget"); + let drive_config = DriveConfig::default(); + + let in_on_resolved_field = WhereClause { + field: "$createdAt".to_string(), + operator: WhereOperator::In, + value: Value::Array(vec![Value::U64(0), Value::U64(7_200_000)]), + }; + let request = DocumentAverageRequest { + contract: &data_contract, + document_type, + sum_property: "amount".to_string(), + where_clauses: vec![in_on_resolved_field], + order_clauses: Vec::new(), + mode: AverageMode::Aggregate, + limit: None, + prove: true, + drive_config: &drive_config, + resolved_time_ranges: vec![ResolvedTimeRange { + transform: dpp::data_contract::document_type::TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 21_600, + step_seconds: 7_200, + phase_seconds: 0, + }, + }], + }; + + let err = drive + .execute_document_average_request(request, None, platform_version) + .expect_err("AVG prove must reject provenance attached to an In clause"); + assert!( + format!("{err:?}").contains("InvalidWhereClauseComponents"), + "expected the provenance shape guard, got: {err:?}" + ); + } + /// `PerInValue` no-proof AVG must honor `request.limit` on the /// returned entry list. Regression for the reviewer's "joint /// dispatcher drops `request.limit`" finding on the PerInValue @@ -2017,6 +2126,7 @@ mod tests { limit: Some(2), prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -2146,6 +2256,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -2252,6 +2363,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive diff --git a/packages/rs-drive/src/query/drive_document_average_query/mod.rs b/packages/rs-drive/src/query/drive_document_average_query/mod.rs index e7576ff2499..35d7975b311 100644 --- a/packages/rs-drive/src/query/drive_document_average_query/mod.rs +++ b/packages/rs-drive/src/query/drive_document_average_query/mod.rs @@ -30,7 +30,7 @@ pub mod drive_dispatcher; #[cfg(feature = "server")] -use crate::query::{OrderClause, WhereClause}; +use crate::query::{OrderClause, ResolvedTimeRange, WhereClause}; #[cfg(feature = "server")] use crate::config::DriveConfig; @@ -114,6 +114,13 @@ pub struct DocumentAverageRequest<'a> { pub sum_property: String, /// Structured where-clauses. pub where_clauses: Vec, + /// The fields among `where_clauses` whose equality clause was produced by + /// `IN_TIME_RANGE` resolution rather than written by the caller. Same + /// contract and same purpose as + /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]: + /// it is what gates which indexes the sum pickers (which average rides) + /// may select. + pub resolved_time_ranges: Vec, /// Structured order-clauses. pub order_clauses: Vec, /// The average mode requested. diff --git a/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs index 1442184ddc7..2dd988f08e9 100644 --- a/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs @@ -37,11 +37,13 @@ use super::super::drive_document_average_query::{ AverageMode, DocumentAverageRequest, DocumentAverageResponse, }; -use super::super::drive_document_count_query::drive_dispatcher::validate_and_canonicalize_where_clauses; use super::super::drive_document_sum_query::mode_detection::detect_sum_mode_from_inputs; use super::super::drive_document_sum_query::{DocumentSumMode, SumMode}; use crate::drive::Drive; use crate::error::Error; +use crate::query::{ + validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes, +}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::version::PlatformVersion; @@ -111,6 +113,8 @@ impl Drive { // the catalog of rejections. let where_clauses = validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; + let resolved_time_ranges = request.resolved_time_ranges; + validate_resolved_time_range_clause_shapes(&where_clauses, &resolved_time_ranges)?; // Convert AverageMode → SumMode (1:1 by construction); sum's // routing table is the single source of truth for the @@ -145,6 +149,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, transaction, platform_version, @@ -170,6 +175,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, order_by_ascending, per_in_limit as u16, @@ -214,6 +220,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, return_distinct, order_by_ascending, diff --git a/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/per_in_value.rs b/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/per_in_value.rs index 5a88ea33ecf..c4a3eeaef81 100644 --- a/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/per_in_value.rs +++ b/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/per_in_value.rs @@ -22,6 +22,7 @@ use super::super::super::drive_document_sum_query::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::{WhereClause, WhereOperator}; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -48,6 +49,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, left_to_right: bool, limit: u16, @@ -122,6 +124,7 @@ impl Drive { document_type.indexes(), &clauses_for_value, &sum_property, + resolved_time_ranges, ) .filter(|idx| idx.countable.is_countable()) .ok_or_else(|| { diff --git a/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/range_no_proof.rs b/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/range_no_proof.rs index 70fbbf09ab5..7fc349b0d0f 100644 --- a/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/range_no_proof.rs +++ b/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/range_no_proof.rs @@ -41,6 +41,7 @@ use super::super::super::drive_document_sum_query::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::{WhereClause, WhereOperator}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; @@ -71,6 +72,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, return_distinct: bool, left_to_right: bool, @@ -82,6 +84,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .filter(|idx| idx.range_countable) .ok_or_else(|| { diff --git a/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/total.rs b/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/total.rs index 1ea08baea67..facc40b8d8d 100644 --- a/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/total.rs +++ b/packages/rs-drive/src/query/drive_document_count_and_sum_query/executors/total.rs @@ -32,6 +32,7 @@ use super::super::super::drive_document_sum_query::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -54,6 +55,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -89,6 +91,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .filter(|idx| idx.countable.is_countable()) .ok_or_else(|| { diff --git a/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs index 6237e3f009a..7f865587fc9 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs @@ -22,6 +22,12 @@ use super::{DocumentCountMode, DriveDocumentCountQuery, SplitCountEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; +// Shared with the sum / average / joint dispatchers and the SDK proof +// verifiers — see `crate::query::canonicalize` for the shape contract. +use crate::query::{ + validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes, +}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -62,6 +68,12 @@ pub struct DocumentCountRequest<'a> { /// the catalog of rejections this enables and the In/range + /// `between*` canonicalization rules) before mode detection. pub where_clauses: Vec, + /// The fields among `where_clauses` whose equality clause was produced by + /// `IN_TIME_RANGE` resolution rather than written by the caller. Same + /// contract and same purpose as + /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]: + /// it is what gates which indexes the count pickers may select. + pub resolved_time_ranges: Vec, /// Structured `order_by` clauses. The first clause's direction /// governs split-mode entry ordering (per-`In`-value / /// per-distinct-value-in-range) and, on the @@ -196,178 +208,6 @@ pub fn where_clauses_from_value( validate_and_canonicalize_where_clauses(clauses, platform_version) } -/// Run the system-wide where-clause validator on a structured -/// `Vec` and canonicalize same-field range pairs into -/// their `between*` form. Single source of truth for the -/// count-endpoint shape contract; called both from the legacy -/// CBOR-decoded entry [`where_clauses_from_value`] and from the -/// dispatcher's typed entry, [`Drive::execute_document_count_request`]. -/// -/// The validator (`WhereClause::group_clauses`) rejects: -/// - Duplicate `Equal` clauses on the same field -/// (`DuplicateNonGroupableClauseSameField`). -/// - Multiple `In` clauses (`MultipleInClauses`) — rejected here: the -/// shared grammar accepts them for protocol version 14+ document -/// queries, but the aggregate surfaces do not. -/// - Multiple non-groupable range clauses (`MultipleRangeClauses`). -/// - Equality + `In` on the same field, range + equality/In on the -/// same field (`DuplicateNonGroupableClauseSameField` / -/// `InvalidWhereClauseComponents`). -/// -/// Without this validation, downstream -/// [`DriveDocumentCountQuery::find_countable_index_for_where_clauses`] -/// collapses repeated fields into a `BTreeSet` and -/// [`DriveDocumentCountQuery::point_lookup_count_path_query`] -/// resolves each index property with a single `.find(...)` — both -/// of which silently pick the first clause on a duplicated field -/// and return a count for an arbitrarily reduced query rather than -/// rejecting the malformed request. -/// -/// **Exception**: `MultipleRangeClauses` is intentionally tolerated -/// here. The regular-query parser rejects two ranges on different -/// fields wholesale (its callers expect -/// `(equal_clauses, in_clause, range_clause)` triples), but the -/// count-query path accepts the carrier-aggregate shape -/// (`outer_range + inner_ACOR_range` on different fields, e.g. -/// G8). Structural validation for that shape lives in -/// [`DriveDocumentCountQuery::detect_mode`] (which knows about -/// `CountMode::GroupByRange`-with-two-ranges and routes to -/// `DocumentCountMode::RangeAggregateCarrierProof`); replicating -/// it here would be redundant. -/// -/// After validation, [`merge_same_field_range_pairs`] collapses -/// `[field > A, field < B]` (and analogous pairs with `>=` / `<=`) -/// into the canonical `between*` operator that -/// [`DriveDocumentCountQuery::range_clause_to_query_item`] knows -/// how to convert into a single `QueryItem`. The regular-query -/// parser does the same merge before its grouped-triple -/// validation; for count queries we do it explicitly here so -/// callers can pass either the bounded form (e.g. -/// `[brand > A, brand < B]`) or the pre-merged form (e.g. -/// `[brand BetweenExcludeBounds [A, B]]`) and get equivalent -/// mode detection downstream. Without this merge, G8a's natural -/// wire shape (four range clauses, two per field) would slip past -/// the catch-`MultipleRangeClauses` block above and then get -/// rejected by `detect_mode`'s `range_count > 1` structural check. -pub fn validate_and_canonicalize_where_clauses( - clauses: Vec, - platform_version: &PlatformVersion, -) -> Result, Error> { - match WhereClause::group_clauses(&clauses, platform_version) { - // Multiple `In` clauses are a document-query-only shape (protocol - // version 14+); the aggregate surfaces keep rejecting them since - // their mode detection and index pickers assume a single `In`. - Ok((_, _, in_clauses)) if in_clauses.len() > 1 => { - return Err(Error::Query(QuerySyntaxError::MultipleInClauses( - "aggregate queries support at most one in clause", - ))); - } - Ok(_) => {} - Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(_))) => {} - Err(e) => return Err(e), - } - merge_same_field_range_pairs(clauses) -} - -/// Collapse `[field > A, field < B]` (and analogous pairs with -/// `>=` / `<=`) into a single `field between* [A, B]` clause per -/// field. Equality / In clauses pass through unchanged. -/// -/// Returns an error if a field has more than two range clauses -/// (structurally meaningless — a third bound would either -/// contradict an existing one or be redundant) or if the pair -/// isn't one lower-bound + one upper-bound (e.g. two `>` on the -/// same field). -fn merge_same_field_range_pairs(clauses: Vec) -> Result, Error> { - use crate::query::conditions::WhereOperator::{ - Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, GreaterThan, - GreaterThanOrEquals, LessThan, LessThanOrEquals, - }; - use std::collections::BTreeMap; - - let mut by_field: BTreeMap> = BTreeMap::new(); - let mut non_range: Vec = Vec::new(); - for wc in clauses { - if DriveDocumentCountQuery::is_range_operator(wc.operator) { - by_field.entry(wc.field.clone()).or_default().push(wc); - } else { - non_range.push(wc); - } - } - let mut result = non_range; - for (field, mut ranges) in by_field { - match ranges.len() { - 0 => {} - 1 => result.push(ranges.remove(0)), - 2 => { - let (mut lower, mut upper): (Option, Option) = - (None, None); - for r in ranges { - match r.operator { - GreaterThan | GreaterThanOrEquals => { - if lower.is_some() { - return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "two lower-bound range clauses on the same field cannot be \ - merged; combine via `between*` or remove the redundant clause", - ))); - } - lower = Some(r); - } - LessThan | LessThanOrEquals => { - if upper.is_some() { - return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "two upper-bound range clauses on the same field cannot be \ - merged; combine via `between*` or remove the redundant clause", - ))); - } - upper = Some(r); - } - _ => { - // The other range operators (Between*, - // StartsWith) are themselves bounded - // already; a second range clause on the - // same field is structurally redundant. - return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "cannot pair a `between*`/`startsWith` range clause with \ - another range on the same field; use the pre-merged form", - ))); - } - } - } - let lower = lower.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "two range clauses on the same field require one lower bound (> or >=) \ - and one upper bound (< or <=)", - )))?; - let upper = upper.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "two range clauses on the same field require one lower bound (> or >=) \ - and one upper bound (< or <=)", - )))?; - let merged_op = match ( - lower.operator == GreaterThanOrEquals, - upper.operator == LessThanOrEquals, - ) { - (true, true) => Between, // [a, b] - (false, false) => BetweenExcludeBounds, // (a, b) - (true, false) => BetweenExcludeRight, // [a, b) - (false, true) => BetweenExcludeLeft, // (a, b] - }; - result.push(WhereClause { - field, - operator: merged_op, - value: dpp::platform_value::Value::Array(vec![lower.value, upper.value]), - }); - } - _ => { - return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "more than two range clauses on the same field are not supported; a \ - bounded range needs exactly one lower bound and one upper bound", - ))); - } - } - } - Ok(result) -} - /// Parse the decoded `order_by` value into structured [`OrderClause`]s. /// /// Same shape as [`where_clauses_from_value`] for `order_by`: @@ -445,6 +285,8 @@ impl Drive { // for the catalog of rejections / canonicalization rules. let where_clauses = validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; + let resolved_time_ranges = request.resolved_time_ranges; + validate_resolved_time_range_clause_shapes(&where_clauses, &resolved_time_ranges)?; let order_clauses = request.order_clauses; // Split-mode entry direction is whatever the first orderBy @@ -476,6 +318,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, transaction, platform_version, )?; @@ -497,6 +340,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, options, transaction, platform_version, @@ -537,6 +381,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, options, transaction, platform_version, @@ -558,6 +403,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, transaction, platform_version, )?, @@ -617,6 +463,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, limit_u16, left_to_right, transaction, @@ -630,6 +477,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, transaction, platform_version, )?, @@ -717,6 +565,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, effective_limit, left_to_right, transaction, diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/per_in_value.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/per_in_value.rs index 5c9778d1efe..3ab683cdf8d 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/per_in_value.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/per_in_value.rs @@ -9,6 +9,7 @@ use super::super::{DriveDocumentCountQuery, SplitCountEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; use grovedb::TransactionArg; @@ -37,6 +38,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], options: RangeCountOptions, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -97,6 +99,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &clauses_for_value, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/point_lookup_proof.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/point_lookup_proof.rs index a483f1343d1..56b31399008 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/point_lookup_proof.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/point_lookup_proof.rs @@ -9,6 +9,7 @@ use super::super::DriveDocumentCountQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -32,12 +33,14 @@ impl Drive { /// fast path doesn't apply), rejects with /// `WhereClauseOnNonIndexedProperty`. Same contract on both /// prove and no-proof paths — no silent fallback. + #[allow(clippy::too_many_arguments)] pub fn execute_document_count_point_lookup_proof( &self, contract_id: [u8; 32], document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -69,6 +72,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/range_aggregate_carrier_proof.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/range_aggregate_carrier_proof.rs index 3a782e1ae68..4f236843b68 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/range_aggregate_carrier_proof.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/range_aggregate_carrier_proof.rs @@ -27,6 +27,7 @@ use super::super::DriveDocumentCountQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -55,6 +56,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], limit: Option, left_to_right: bool, transaction: TransactionArg, @@ -63,6 +65,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/range_distinct_proof.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/range_distinct_proof.rs index 3a340a22454..de6841417fb 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/range_distinct_proof.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/range_distinct_proof.rs @@ -10,6 +10,7 @@ use super::super::DriveDocumentCountQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -36,6 +37,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], limit: u16, left_to_right: bool, transaction: TransactionArg, @@ -44,6 +46,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/range_no_proof.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/range_no_proof.rs index 4b2e677daf7..998cdf8da00 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/range_no_proof.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/range_no_proof.rs @@ -10,6 +10,7 @@ use super::super::{DriveDocumentCountQuery, SplitCountEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -26,6 +27,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], options: RangeCountOptions, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -33,6 +35,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/range_proof.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/range_proof.rs index 3436eeae738..9a8835fa9e6 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/range_proof.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/range_proof.rs @@ -9,6 +9,7 @@ use super::super::DriveDocumentCountQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -18,18 +19,21 @@ impl Drive { /// Range-count proof via grovedb's `AggregateCountOnRange`. /// Returns proof bytes that the client verifies via /// `GroveDb::verify_aggregate_count_query`. + #[allow(clippy::too_many_arguments)] pub fn execute_document_count_range_proof( &self, contract_id: [u8; 32], document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/executors/total.rs b/packages/rs-drive/src/query/drive_document_count_query/executors/total.rs index 6a00660b4c5..74770889d79 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/executors/total.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/executors/total.rs @@ -6,6 +6,7 @@ use super::super::{DriveDocumentCountQuery, SplitCountEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; use grovedb::TransactionArg; @@ -18,12 +19,14 @@ impl Drive { /// tree's root). /// /// Single summed entry with empty key. + #[allow(clippy::too_many_arguments)] pub fn execute_document_count_total_no_proof( &self, contract_id: [u8; 32], document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -56,6 +59,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_count_query/index_picker.rs b/packages/rs-drive/src/query/drive_document_count_query/index_picker.rs index 30ff6ed939c..9c3f4a773a1 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/index_picker.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/index_picker.rs @@ -6,6 +6,8 @@ use super::super::conditions::WhereClause; use super::DriveDocumentCountQuery; +use crate::query::index_admissible_for_resolved_time_range; +use crate::query::ResolvedTimeRange; use dpp::data_contract::document_type::Index; use std::collections::{BTreeMap, BTreeSet}; @@ -33,9 +35,16 @@ impl DriveDocumentCountQuery<'_> { /// clauses), the dispatcher reads the document-type primary-key tree's /// CountTree directly — that path doesn't use this picker because no /// index is involved. + /// + /// `resolved_time_ranges` names the fields whose equality clause was + /// produced by `IN_TIME_RANGE` resolution (see + /// [`crate::query::resolve_time_range_bucket_clause`]); it gates which + /// indexes are candidates at all — see + /// [`index_admissible_for_resolved_time_range`]. pub fn find_countable_index_for_where_clauses<'b>( indexes: &'b BTreeMap, where_clauses: &[WhereClause], + resolved_time_ranges: &[ResolvedTimeRange], ) -> Option<&'b Index> { if Self::has_unsupported_operator(where_clauses) { return None; @@ -56,6 +65,14 @@ impl DriveDocumentCountQuery<'_> { } for index in indexes.values() { + // A time-range index holds one entry per bucket containing the + // document, keyed by bucket start: counting over it multi-counts + // every document unless the query pins a single bucket, and only + // a resolution-produced equality does that. Conversely a raw + // clause must never bind to bucket keys. + if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) { + continue; + } if !index.countable.is_countable() { continue; } @@ -93,9 +110,18 @@ impl DriveDocumentCountQuery<'_> { /// walks the current model doesn't support). Pure point-lookup queries /// (no range operator) should fall back to /// [`Self::find_countable_index_for_where_clauses`]. + /// + /// `resolved_time_ranges` gates the candidate set exactly as in + /// [`Self::find_countable_index_for_where_clauses`]. A resolved field + /// never arrives as a range clause — resolution always produces an + /// equality — so with a non-empty list the only bucketed index this can + /// return is one whose resolved equality is a prefix property and whose + /// range terminator is a different property. That is the intended shape: + /// a range over one property within a single time bucket. pub fn find_range_countable_index_for_where_clauses<'b>( indexes: &'b BTreeMap, where_clauses: &[WhereClause], + resolved_time_ranges: &[ResolvedTimeRange], ) -> Option<&'b Index> { let range_clauses: Vec<&WhereClause> = where_clauses .iter() @@ -147,6 +173,13 @@ impl DriveDocumentCountQuery<'_> { .collect(); for index in indexes.values() { + // Same admissibility rule as the point-lookup picker: bucketed + // indexes store one entry per containing bucket, so only a query + // pinned to a single bucket by a resolution-produced equality may + // walk them, and raw clauses may never bind to bucket keys. + if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) { + continue; + } if !index.range_countable || !index.countable.is_countable() { continue; } @@ -174,14 +207,26 @@ impl DriveDocumentCountQuery<'_> { // properties. For the widget contract there are no // such middle properties on byBrandColor, but the // builder handles the general case. + let intermediate_props = &index.properties[1..index.properties.len() - 1]; let mut intermediate_props_ok = true; - for prop in &index.properties[1..index.properties.len() - 1] { + for prop in intermediate_props { if !prefix_fields.contains(prop.name.as_str()) { intermediate_props_ok = false; break; } } - if intermediate_props_ok { + // Strict-coverage check, mirroring sum's picker: every + // Equal/In prefix field must appear in the index's + // intermediate properties. Without this + // `intermediate_props.len() == prefix_fields.len()` guard, + // a query with extra prefix fields would silently pick an + // index that *doesn't* cover them — the carrier path-query + // builder iterates only index properties, so the uncovered + // clause would simply be dropped and the per-group counts + // would span all its values (an over-broad result that + // even verifies, since the verifier rebuilds the same + // path query from the same picker). + if intermediate_props_ok && intermediate_props.len() == prefix_fields.len() { return Some(index); } continue; diff --git a/packages/rs-drive/src/query/drive_document_count_query/path_query.rs b/packages/rs-drive/src/query/drive_document_count_query/path_query.rs index 8018b8367f2..041ade14dd4 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/path_query.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/path_query.rs @@ -223,7 +223,7 @@ impl DriveDocumentCountQuery<'_> { ), )); } - path.push(prop.name.as_bytes().to_vec()); + path.push(self.index.level_key_for_property(&prop.name).into_bytes()); path.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -240,7 +240,11 @@ impl DriveDocumentCountQuery<'_> { ), ))? .name; - path.push(range_prop_name.as_bytes().to_vec()); + path.push( + self.index + .level_key_for_property(range_prop_name) + .into_bytes(), + ); Ok(PathQuery::new_aggregate_count_on_range(path, query_item)) } @@ -363,7 +367,7 @@ impl DriveDocumentCountQuery<'_> { )?; match (&carrier, clause.operator) { (Carrier::Pending, WhereOperator::Equal) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); base_path.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -371,15 +375,16 @@ impl DriveDocumentCountQuery<'_> { )?); } (Carrier::Pending, WhereOperator::In) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); carrier = Carrier::In(clause.clone()); } (Carrier::Pending, op) if Self::is_range_operator(op) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); carrier = Carrier::Range(clause.clone()); } (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => { - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension + .push(self.index.level_key_for_property(&prop.name).into_bytes()); subquery_path_extension.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -404,7 +409,11 @@ impl DriveDocumentCountQuery<'_> { } } } - subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec()); + subquery_path_extension.push( + self.index + .level_key_for_property(terminator_prop_name) + .into_bytes(), + ); let mut outer_query = Query::new_with_direction(left_to_right); match carrier { @@ -582,10 +591,11 @@ impl DriveDocumentCountQuery<'_> { platform_version, )?; if in_outer_keys.is_some() { - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension + .push(self.index.level_key_for_property(&prop.name).into_bytes()); subquery_path_extension.push(serialized); } else { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); base_path.push(serialized); } } @@ -600,7 +610,7 @@ impl DriveDocumentCountQuery<'_> { } // Path stops at the In-bearing prop's property- // name subtree; outer Query lives at that level. - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); let in_values = clause.in_values().into_data_with_error()??; let mut keys: Vec> = in_values .iter() @@ -843,7 +853,11 @@ impl DriveDocumentCountQuery<'_> { let mut in_outer_keys: Option>> = None; let mut subquery_path_extension: Vec> = vec![]; - for prop in self.index.properties.iter() { + for (position, prop) in self.index.properties.iter().enumerate() { + // The path segment is the level key — grid-qualified for a + // time-range index's first property — while the clause lookup + // and value serialization stay on the bare property name. + let level_key = self.index.level_key(position, &prop.name); let clause = self .where_clauses .iter() @@ -871,10 +885,10 @@ impl DriveDocumentCountQuery<'_> { // path. Any number of these may accumulate — // one for each Equal that sits *after* the In // in the index ordering. - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension.push(level_key.as_bytes().to_vec()); subquery_path_extension.push(serialized); } else { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(level_key.as_bytes().to_vec()); base_path.push(serialized); } } @@ -891,7 +905,7 @@ impl DriveDocumentCountQuery<'_> { // property-name subtree; outer Query lives at // that level. Any trailing Equal property then // routes through `subquery_path_extension`. - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); let in_values = clause.in_values().into_data_with_error()??; let mut keys: Vec> = in_values .iter() 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 116147110a5..c8e6cc7986c 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 @@ -1,5 +1,6 @@ use super::*; use crate::drive::Drive; +use crate::query::ResolvedTimeRange; use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; use crate::util::storage_flags::StorageFlags; @@ -142,6 +143,7 @@ fn test_count_query_fully_covered_equal_succeeds_on_both_paths() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&age_eq_30), + &[], ) .expect("expected picker to accept fully-covered byAge index"); @@ -200,6 +202,7 @@ fn test_count_query_picker_rejects_partial_coverage() { let no_match = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &[], + &[], ); assert!( no_match.is_none(), @@ -217,6 +220,7 @@ fn test_count_query_picker_rejects_partial_coverage() { let no_match_partial = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &first_name_only, + &[], ); assert!( no_match_partial.is_none(), @@ -234,6 +238,7 @@ fn test_count_query_picker_rejects_partial_coverage() { let picked = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &age_only, + &[], ) .expect("byAge is exactly covered"); assert_eq!(picked.properties.len(), 1); @@ -267,6 +272,7 @@ fn test_find_countable_index_for_where_clauses_no_match() { let result = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &[where_clause], + &[], ); assert!( @@ -342,6 +348,7 @@ fn test_find_countable_index_rejects_unsupported_operator() { DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(>_clause), + &[], ) .is_none() ); @@ -374,6 +381,7 @@ fn test_count_query_total_count_with_in_operator() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&in_clause), + &[], ) .expect("expected to find countable index for In on age"); @@ -419,6 +427,7 @@ fn test_count_query_total_count_with_in_operator_no_matches() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&in_clause), + &[], ) .expect("expected to find countable index for In on age"); @@ -533,6 +542,7 @@ fn test_aggregate_count_in_fan_out_ignores_default_query_limit() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -603,6 +613,7 @@ fn test_count_query_in_operator_rejects_duplicate_values() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&in_clause), + &[], ) .expect("expected to find countable index for In on age"); @@ -691,6 +702,7 @@ fn test_count_query_in_on_before_last_with_trailing_equal_succeeds_on_both_paths let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("expected picker to accept byFirstNameLastName for In + Equal coverage"); // Sanity-check the picker really chose the 2-prop index, not the @@ -814,6 +826,7 @@ fn test_count_query_in_on_first_of_three_with_two_trailing_equals_succeeds_on_bo let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("expected picker to accept the 3-prop covering index"); // Sanity-pin the picker actually chose the 3-prop unique @@ -943,6 +956,7 @@ fn test_point_lookup_proof_omits_absent_in_branches_from_entries() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&in_clause), + &[], ) .expect("expected picker to accept byAge for In on age"); // Sanity-pin the picker chose the single-property `byAge` index — @@ -1092,6 +1106,7 @@ fn test_count_query_in_operator_accepts_max_sized_array() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&in_clause), + &[], ) .expect("expected picker to accept byAge for In on age"); assert_eq!(index.properties.len(), 1); @@ -1331,6 +1346,7 @@ fn test_compound_range_in_summed_no_proof_uses_per_in_aggregate_fanout() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -1405,6 +1421,7 @@ fn test_count_request_with_duplicate_equality_clauses_is_rejected() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let err = drive @@ -1601,6 +1618,7 @@ fn test_range_distinct_proof_uses_compile_time_default_query_limit_not_operator_ limit: None, prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -1629,6 +1647,7 @@ fn test_range_distinct_proof_uses_compile_time_default_query_limit_not_operator_ let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&color_gt_blue), + &[], ) .expect("byColor range_countable index covers `color > blue`"); let count_query = DriveDocumentCountQuery { @@ -1724,6 +1743,7 @@ fn test_range_distinct_no_proof_rejects_zero_effective_limit() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let result = drive.execute_document_count_request(request, None, platform_version); @@ -1770,6 +1790,7 @@ fn test_count_query_in_operator_rejects_oversized_array() { document_type, "person".to_string(), vec![in_clause], + &[], super::RangeCountOptions { distinct: false, limit: Some(50), @@ -2006,6 +2027,7 @@ fn test_countable_allowing_offset_variant_end_to_end() { let picked = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&first_name_eq_alice), + &[], ) .expect("expected picker to accept CountableAllowingOffset index"); assert_eq!(picked.countable, IndexCountability::CountableAllowingOffset); @@ -2079,6 +2101,7 @@ fn test_count_query_unique_countable_index_returns_correct_count() { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + &[], ) .expect("expected to find a countable index covering all 3 properties"); @@ -2146,6 +2169,7 @@ mod range_countable_picker_tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } @@ -2171,6 +2195,7 @@ mod range_countable_picker_tests { let picked = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ); assert!(picked.is_some()); assert_eq!(picked.unwrap().name, "byColor"); @@ -2204,6 +2229,7 @@ mod range_countable_picker_tests { let picked = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ); assert!(picked.is_some()); assert_eq!(picked.unwrap().name, "byBrandColor"); @@ -2230,6 +2256,7 @@ mod range_countable_picker_tests { DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ) .is_none(), "a range on a non-terminator property must not match — the storage \ @@ -2237,6 +2264,89 @@ mod range_countable_picker_tests { ); } + /// Carrier arm (two ranges on distinct fields): an extra Equal clause + /// on a field the index does not carry must disqualify the index — + /// the carrier path-query builder iterates only index properties, so + /// an admitted index would silently drop the clause and produce an + /// over-broad per-group count that still verifies (the verifier + /// rebuilds the same path query from the same picker). Mirrors sum's + /// strict-coverage guard. + #[test] + fn carrier_arm_rejects_index_missing_an_equality_field() { + let indexes = make_indexes(vec![make_index( + "byBrandColor", + &["brand", "color"], + IndexCountability::Countable, + true, + )]); + let where_clauses = vec![ + WhereClause { + field: "brand".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("a".to_string()), + }, + WhereClause { + field: "color".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("f".to_string()), + }, + WhereClause { + field: "material".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("wood".to_string()), + }, + ]; + assert!( + DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &[], + ) + .is_none(), + "an equality clause the index cannot cover must disqualify it" + ); + } + + /// Positive control for the strict-coverage guard: the identical + /// query shape against an index that carries the equality field as + /// its intermediate property is covered and picked. + #[test] + fn carrier_arm_picks_index_covering_the_equality_field() { + let indexes = make_indexes(vec![make_index( + "byBrandMaterialColor", + &["brand", "material", "color"], + IndexCountability::Countable, + true, + )]); + let where_clauses = vec![ + WhereClause { + field: "brand".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("a".to_string()), + }, + WhereClause { + field: "color".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("f".to_string()), + }, + WhereClause { + field: "material".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("wood".to_string()), + }, + ]; + let picked = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &[], + ); + assert_eq!( + picked.map(|index| index.name.as_str()), + Some("byBrandMaterialColor"), + "with the equality field covered, the carrier index is picked" + ); + } + /// An index without `range_countable: true` must not match even if /// the property structure aligns. The storage layout for these is /// plain NormalTree — no CountTree counts to walk. @@ -2257,6 +2367,7 @@ mod range_countable_picker_tests { DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ) .is_none() ); @@ -2288,6 +2399,7 @@ mod range_countable_picker_tests { DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ) .is_none(), "two separate range operators must be rejected (use Between to express a bounded range)" @@ -2313,6 +2425,7 @@ mod range_countable_picker_tests { DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ) .is_none(), "no range operator → not the range picker's job" @@ -2900,6 +3013,7 @@ mod range_countable_point_lookup_tests { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&brand_eq), + &[], ) .expect("byBrand covers brand==acme"); assert!( @@ -3016,6 +3130,7 @@ mod range_countable_point_lookup_tests { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&brand_in), + &[], ) .expect("byBrand covers brand IN [...]"); assert!(index.range_countable); @@ -3183,6 +3298,7 @@ mod range_countable_point_lookup_tests { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &clauses, + &[], ) .expect("byBrandColor covers brand IN + color ="); assert!(index.range_countable); @@ -3302,6 +3418,7 @@ mod range_countable_point_lookup_tests { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), std::slice::from_ref(&category_eq), + &[], ) .expect("byCategory covers category=tools"); assert!( @@ -3375,3 +3492,217 @@ mod range_countable_point_lookup_tests { assert_eq!(summed, 2); } } + +#[cfg(test)] +mod time_range_picker_tests { + //! Coverage for the transform gate the count pickers apply before + //! scoring a candidate — see + //! [`crate::query::index_admissible_for_resolved_time_range`]. + //! + //! A bucketed index stores one entry per bucket containing a document, + //! keyed by bucket start. Counting over it is only meaningful when the + //! query pins a single bucket, and the only thing that can pin one is an + //! equality produced by `IN_TIME_RANGE` resolution. Both directions of + //! the mismatch return a wrong count rather than an error, so the picker + //! is where they have to be stopped. + + use super::*; + use dpp::data_contract::document_type::{ + Index, IndexCountability, IndexProperty, TimeRangeTransform, + }; + + /// One hour as a transform declares a window (seconds) and as the clause + /// values below are expressed (milliseconds, the unit of a bucket start). + const HOUR_SECONDS: u64 = 3_600; + const HOUR_MS: u64 = 3_600_000; + const SOURCE: &str = "$createdAt"; + + fn make_index( + name: &str, + properties: &[&str], + time_range: Option, + ) -> Index { + Index { + name: name.to_string(), + properties: properties + .iter() + .map(|p| IndexProperty { + name: p.to_string(), + ascending: true, + }) + .collect(), + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::Countable, + range_countable: false, + summable: None, + range_summable: false, + ranked_countable: false, + ranked_summable: false, + ranked_averageable: false, + time_range, + } + } + + /// `trending` buckets `$createdAt` into 6h windows every 2h; `byHashtag` + /// covers the same two fields with raw timestamps and sorts first. + fn indexes() -> std::collections::BTreeMap { + [ + make_index("byHashtag", &["hashtag", SOURCE], None), + make_index( + "trending", + &[SOURCE, "hashtag"], + Some(TimeRangeTransform { + source: SOURCE.to_string(), + range_seconds: 6 * HOUR_SECONDS, + step_seconds: 2 * HOUR_SECONDS, + phase_seconds: 0, + }), + ), + ] + .into_iter() + .map(|index| (index.name.clone(), index)) + .collect() + } + + /// The provenance a real `IN_TIME_RANGE` resolution against the + /// `trending` grid produces: the source field plus the exact transform. + /// Constructed directly (not read from the candidate map) so tests that + /// remove the trending index can still present the resolution. + fn source_resolution() -> Vec { + vec![ResolvedTimeRange { + transform: TimeRangeTransform { + source: SOURCE.to_string(), + range_seconds: 6 * HOUR_SECONDS, + step_seconds: 2 * HOUR_SECONDS, + phase_seconds: 0, + }, + }] + } + + fn equal(field: &str, value: Value) -> WhereClause { + WhereClause { + field: field.to_string(), + operator: WhereOperator::Equal, + value, + } + } + + /// The resolved equality names the bucketed index's source, so that index + /// — and only that index — may serve the count. + #[test] + fn resolved_source_equality_selects_the_bucketed_index() { + let indexes = indexes(); + let where_clauses = vec![ + equal(SOURCE, Value::U64(6 * HOUR_MS)), + equal("hashtag", Value::Text("ibiza".to_string())), + ]; + let picked = DriveDocumentCountQuery::find_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &source_resolution(), + ) + .expect("the bucketed index exactly covers the resolved clause set"); + assert_eq!(picked.name, "trending"); + } + + /// The same clause set without the provenance must not reach the bucketed + /// index. `byHashtag` covers it and sorts first, so this also pins that + /// the gate does not merely reorder candidates. + #[test] + fn raw_equality_on_the_source_never_selects_the_bucketed_index() { + let indexes = indexes(); + let where_clauses = vec![ + equal(SOURCE, Value::U64(1_700_000_000_000)), + equal("hashtag", Value::Text("ibiza".to_string())), + ]; + let picked = DriveDocumentCountQuery::find_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &[], + ) + .expect("the plain index covers a raw equality on both fields"); + assert_eq!(picked.name, "byHashtag"); + } + + /// An `In` over the source enumerates bucket starts, and a document + /// appears under every bucket containing it, so any hit would be counted + /// once per overlapping bucket. Resolution never produces an `In`, so the + /// only way this shape reaches the picker is a client writing it by hand. + #[test] + fn raw_in_on_the_source_does_not_select_the_bucketed_index() { + let mut indexes = indexes(); + // Drop the plain index so a `None` here can only mean the bucketed + // one was refused, not that a raw index happened to win. + indexes.remove("byHashtag"); + let where_clauses = vec![ + WhereClause { + field: SOURCE.to_string(), + operator: WhereOperator::In, + value: Value::Array(vec![ + Value::U64(2 * HOUR_MS), + Value::U64(4 * HOUR_MS), + Value::U64(6 * HOUR_MS), + ]), + }, + equal("hashtag", Value::Text("ibiza".to_string())), + ]; + assert!( + DriveDocumentCountQuery::find_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &[], + ) + .is_none() + ); + } + + /// The converse gate: with a resolved field named, an index that stores + /// raw timestamps is not a candidate even when it exactly covers the + /// clause fields — matching a bucket start against raw values would + /// return a proven-empty result. + #[test] + fn plain_index_is_not_selected_when_a_time_range_field_was_resolved() { + let mut indexes = indexes(); + indexes.remove("trending"); + let where_clauses = vec![ + equal(SOURCE, Value::U64(6 * HOUR_MS)), + equal("hashtag", Value::Text("ibiza".to_string())), + ]; + assert!( + DriveDocumentCountQuery::find_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &source_resolution(), + ) + .is_none() + ); + } + + /// Provenance names its field through the transform itself + /// ([`ResolvedTimeRange::field`] is derived from `transform.source`), so + /// the fabricated field/transform mismatch this test used to construct is + /// unrepresentable. What remains fabricatable is a resolution whose grid + /// no index declares — it must admit nothing. + #[test] + fn provenance_with_a_grid_no_index_declares_admits_nothing() { + let indexes = indexes(); + let where_clauses = vec![ + equal(SOURCE, Value::U64(6 * HOUR_MS)), + equal("hashtag", Value::Text("ibiza".to_string())), + ]; + let mut mismatched = source_resolution(); + mismatched[0].transform.step_seconds /= 2; + assert!( + DriveDocumentCountQuery::find_countable_index_for_where_clauses( + &indexes, + &where_clauses, + &mismatched, + ) + .is_none(), + "a resolution carrying a grid no index declares must not admit \ + the bucketed index (nor, being a resolution, the plain one)" + ); + } +} diff --git a/packages/rs-drive/src/query/drive_document_having_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_having_query/drive_dispatcher.rs index b484460081b..77887a6b729 100644 --- a/packages/rs-drive/src/query/drive_document_having_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_having_query/drive_dispatcher.rs @@ -5,10 +5,11 @@ use super::super::drive_document_ranked_query::{RankedEntry, RankedPaginationInputs}; use super::mode_detection::detect_having_mode; use crate::drive::Drive; +use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::having::HavingClause; use crate::query::projection::SelectProjection; -use crate::query::{OrderClause, WhereClause}; +use crate::query::{OrderClause, ResolvedTimeRange, WhereClause}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -47,6 +48,16 @@ pub struct DocumentHavingRequest<'a> { /// equality pins on the covering compound index's leading /// properties for the pinned-prefix form. pub where_clauses: &'a [WhereClause], + /// The fields among `where_clauses` whose equality clause was produced by + /// `IN_TIME_RANGE` resolution (see + /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]). + /// Must be empty: HAVING's equality prefixes pin plain ranked indexes + /// (its picker excludes transformed ones), so a resolved bucket-start + /// equality would authenticate raw-timestamp matches at the bucket + /// boundary instead of window membership. Carried (and rejected) here + /// for the same reason the ranked request carries it: drive owns the + /// rejection regardless of which upstream path built the request. + pub resolved_time_ranges: &'a [ResolvedTimeRange], /// Request `limit`. **Required**; `1 ..= MAX_HAVING_LIMIT`. pub limit: Option, /// Request `offset`. Must be `None` — the range walk has no skip. @@ -104,6 +115,20 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { + // Before mode detection reads `where_clauses`: a resolved bucket + // equality is indistinguishable from a hand-written equality pin, and + // the pinned-prefix form would accept it against a plain ranked index + // over raw timestamps — a validly-proven answer to a different + // question. See `DocumentHavingRequest::resolved_time_ranges`. + if !request.resolved_time_ranges.is_empty() { + return Err(Error::Query(QuerySyntaxError::Unsupported( + "a HAVING query cannot carry a time-range (IN_TIME_RANGE) selection: its \ + equality prefixes pin plain ranked indexes, so a resolved bucket-start \ + equality would match raw timestamps at the bucket boundary instead of the \ + selected window" + .to_string(), + ))); + } let mode = detect_having_mode( &request.select, request.group_by, diff --git a/packages/rs-drive/src/query/drive_document_having_query/execute_range.rs b/packages/rs-drive/src/query/drive_document_having_query/execute_range.rs index 6707461d8d5..fa02f06804a 100644 --- a/packages/rs-drive/src/query/drive_document_having_query/execute_range.rs +++ b/packages/rs-drive/src/query/drive_document_having_query/execute_range.rs @@ -12,13 +12,15 @@ //! `pub mod execute_range;` declaration. use super::super::drive_document_ranked_query::{RankedAxis, RankedEntry, RankedEntryValue}; -use super::{AxisRangeBounds, DriveDocumentHavingQuery}; +use super::DriveDocumentHavingQuery; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use dpp::version::PlatformVersion; -use grovedb::TransactionArg; +use grovedb::query_result_type::QueryResultType; +use grovedb::{AxisKeys, PathQuery, PathQueryRun, TransactionArg}; use grovedb_costs::CostContext; +use grovedb_query::AxisQuery; impl DriveDocumentHavingQuery<'_> { /// Read the matching groups directly from the axis secondary: every @@ -41,68 +43,73 @@ impl DriveDocumentHavingQuery<'_> { ) -> 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(); + + // The same bounded axis PathQuery the prove path uses, with the + // keys-only projection: the matching pairs are read straight off + // the pinned secondary view, no primary values resolved. + let (lo, hi) = self.bounds.i128_bounds(); + let path_query = PathQuery::new_axis( + path, + AxisQuery::bounded( + self.bounds.axis().into(), + lo, + hi, + self.limit, + self.descending, + ) + .keys_only(), + ); // Costs are destructured away rather than `.unwrap()`-ed, same // as the ranked executors: `CostContext::unwrap` is infallible // but reads like a panicking unwrap at the call site. - let entries = match self.bounds { - AxisRangeBounds::Count { lo, hi } => { - let CostContext { value, cost: _ } = drive.grove.indexed_count_range_keys( - path_refs.as_slice(), - lo, - hi, - self.descending, - self.limit, - transaction, - grove_version, - ); - value - .map_err(|e| Error::GroveDB(Box::new(e)))? - .into_iter() - .map(|(count, key)| RankedEntry { - key, - value: RankedEntryValue::Count(count), - }) - .collect::>() - } - AxisRangeBounds::Sum { lo, hi } => { - let CostContext { value, cost: _ } = drive.grove.indexed_sum_range_keys( - path_refs.as_slice(), - lo, - hi, - self.descending, - self.limit, - transaction, - grove_version, - ); - value - .map_err(|e| Error::GroveDB(Box::new(e)))? - .into_iter() - .map(|(sum, key)| RankedEntry { - key, - value: RankedEntryValue::Sum(sum), - }) - .collect::>() - } - AxisRangeBounds::Avg { lo, hi } => { - let CostContext { value, cost: _ } = drive.grove.indexed_avg_range_keys( - path_refs.as_slice(), - lo, - hi, - self.descending, - self.limit, - transaction, - grove_version, - ); - value - .map_err(|e| Error::GroveDB(Box::new(e)))? - .into_iter() - .map(|(avg, key)| RankedEntry { - key, - value: RankedEntryValue::AvgFixedPoint(avg), - }) - .collect::>() + let CostContext { value, cost: _ } = drive.grove.run_path_query( + &path_query, + true, + true, + true, + QueryResultType::QueryPathKeyElementTrioResultType, + transaction, + grove_version, + ); + // `skipped` is the paginated traversal's field and is `None` + // for bounded ones — nothing to check here. + let PathQueryRun::AxisKeys { keys, skipped: _ } = + value.map_err(|e| Error::GroveDB(Box::new(e)))? + else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "having {:?} range read ran to a non-axis-keys result shape", + self.bounds.axis() + )))); + }; + + let entries = match (self.bounds.axis(), keys) { + (RankedAxis::Count, AxisKeys::Count(pairs)) => pairs + .into_iter() + .map(|(count, key)| RankedEntry { + key, + value: RankedEntryValue::Count(count), + }) + .collect::>(), + (RankedAxis::Sum, AxisKeys::Sum(pairs)) => pairs + .into_iter() + .map(|(sum, key)| RankedEntry { + key, + value: RankedEntryValue::Sum(sum), + }) + .collect::>(), + (RankedAxis::Avg, AxisKeys::Avg(pairs)) => pairs + .into_iter() + .map(|(avg, key)| RankedEntry { + key, + value: RankedEntryValue::AvgFixedPoint(avg), + }) + .collect::>(), + (axis, other) => { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "having {axis:?} range read returned {} pairs of a different axis shape", + other.len() + )))); } }; @@ -120,57 +127,52 @@ impl DriveDocumentHavingQuery<'_> { Ok(entries) } - /// Generate the grovedb indexed-axis range proof for this query. + /// Generate the bounded-axis proof for this query, through the + /// unified `PathQuery` surface (grovedb's only public proof surface + /// for indexed-axis reads): the query is + /// [`PathQuery::new_axis_bounded`] and the envelope is a GroveDBProof + /// V1 carrying an axis descent into the queried secondary. /// /// The envelope commits the in-range secondary entries, 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. The Merk - /// query (the encoded bounds and walk direction) and the limit are - /// echoed and re-checked by grovedb's verifier against the client's - /// own reconstruction via [`AxisRangeBounds::merk_query`] — which is - /// why the bounds are validated rather than clamped upstream, and - /// why completeness needs no extra machinery: a Merk range proof - /// over a sorted keyspace commits its boundaries, so an in-range - /// group the server omitted fails reconstruction. + /// primary's root hash, the sibling axes' root hashes, and the + /// ordinary layer chain up to the grovedb root — so the client + /// reconstructs the platform root hash from it. Nothing is echoed: + /// the verifier takes the client's own reconstruction of the same + /// `PathQuery` as input, and grovedb lowers its bounds into the + /// secondary's keyspace through one function shared by both proof + /// sides — which is why the bounds are validated rather than clamped + /// upstream, and why completeness needs no extra machinery: a Merk + /// range proof over a sorted keyspace commits its boundaries, so an + /// in-range group the server omitted fails reconstruction. /// /// Verified by /// [`DriveDocumentHavingQuery::verify_having_range_proof`](crate::query::DriveDocumentHavingQuery::verify_having_range_proof). pub fn execute_range_with_proof( &self, drive: &Drive, - transaction: TransactionArg, + _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(); - let secondary_query = self.bounds.merk_query(self.descending); + let (lo, hi) = self.bounds.i128_bounds(); + let path_query = PathQuery::new_axis_bounded( + path, + self.bounds.axis().into(), + lo, + hi, + self.limit, + self.descending, + ); + // The unified prover proves committed state — it takes no + // transaction. The parameter is kept for signature stability with + // the no-proof executor; the query dispatch passes `None` on this + // surface anyway (queries answer from committed state). + // // Same destructure-don't-unwrap rationale as the no-proof arm. - let CostContext { value, cost: _ } = match self.bounds.axis() { - RankedAxis::Count => drive.grove.prove_indexed_count_query( - path_refs.as_slice(), - secondary_query, - Some(self.limit), - transaction, - grove_version, - ), - RankedAxis::Sum => drive.grove.prove_indexed_sum_query( - path_refs.as_slice(), - secondary_query, - Some(self.limit), - transaction, - grove_version, - ), - RankedAxis::Avg => drive.grove.prove_indexed_avg_query( - path_refs.as_slice(), - secondary_query, - Some(self.limit), - transaction, - grove_version, - ), - }; + let CostContext { value, cost: _ } = + drive.grove.prove_query(&path_query, None, grove_version); value.map_err(|e| Error::GroveDB(Box::new(e))) } } diff --git a/packages/rs-drive/src/query/drive_document_having_query/mod.rs b/packages/rs-drive/src/query/drive_document_having_query/mod.rs index f0418fc798a..dcf17d0b883 100644 --- a/packages/rs-drive/src/query/drive_document_having_query/mod.rs +++ b/packages/rs-drive/src/query/drive_document_having_query/mod.rs @@ -37,13 +37,15 @@ //! rank `offset`); having-range addresses them by **value bound** //! (`aggregate ∈ [lo, hi]`). Three consequences: //! -//! 1. **The bound is part of the proof contract.** The grovedb envelope -//! for a range read echoes the Merk query itself, and the verifier -//! re-builds that query from the request's bounds -//! ([`AxisRangeBounds::merk_query`]) — so prover and verifier must -//! share one bounds-to-query translation, exactly as they share the -//! grove path. Completeness comes from the Merk range proof: the -//! boundary commitments show no in-range group was omitted. +//! 1. **The bound is part of the proof contract.** Both proof sides +//! build the same [`grovedb::PathQuery::new_axis_bounded`] from the +//! request's bounds ([`AxisRangeBounds::i128_bounds`]), and grovedb +//! lowers those bounds into the secondary's keyspace through one +//! shared function on both the prover and the verifier — so the two +//! sides cannot drift on which range a proof is about, exactly as +//! they share the grove path. Completeness comes from the Merk range +//! proof: the boundary commitments show no in-range group was +//! omitted. //! 2. **No `OFFSET`, no `start_at` — and no full pagination.** The //! range primitives take a limit but no skip, and a request carrying //! either knob is rejected loudly. A page cut at `limit` can only be @@ -85,8 +87,6 @@ use crate::error::query::QuerySyntaxError; use crate::error::Error; #[cfg(any(feature = "server", feature = "verify"))] use grovedb::element::indexed::{encode_avg_sort_key, encode_count_sort_key, encode_sum_sort_key}; -#[cfg(any(feature = "server", feature = "verify"))] -use grovedb::Query; #[cfg(any(feature = "server", feature = "verify"))] pub mod mode_detection; @@ -196,23 +196,26 @@ impl AxisRangeBounds { } } - /// The Merk query over the axis secondary that reads exactly these - /// bounds, walking in the requested direction. + /// The inclusive bounds widened to `i128` — the domain + /// [`grovedb::PathQuery::new_axis_bounded`] takes for every axis. + /// + /// The prover/verifier-agreement artifact of the having surface is + /// grovedb's own bounded-axis lowering: both proof sides lower the + /// same `PathQuery` bounds into the secondary's keyspace through one + /// shared function inside grovedb, producing exactly the byte range + /// [`Self::secondary_key_bounds`] documents — inclusive at + /// `encode(lo)`, exclusive at `encode(hi + 1)`, open-ended at the + /// axis maximum. Platform no longer builds the Merk query itself, so + /// the two sides cannot drift on which range a proof is about. /// - /// This is the **prover/verifier-agreement artifact** of the having - /// surface: grovedb's range-proof envelope is generated against this - /// query and verified against the verifier's own reconstruction of - /// it, so both sides must build it from the same bounds through this - /// one function — a divergence surfaces as a failed verification, - /// not a wrong answer. - pub fn merk_query(&self, descending: bool) -> Query { - let (lower, upper) = self.secondary_key_bounds(); - let mut query = Query::new_with_direction(!descending); - match upper { - Some(upper) => query.insert_range(lower..upper), - None => query.insert_range_from(lower..), + /// Widening is lossless: `u64` and `i64` both embed in `i128`, and + /// grovedb clamps back to each axis's own domain when lowering. + pub fn i128_bounds(&self) -> (i128, i128) { + match *self { + AxisRangeBounds::Count { lo, hi } => (lo as i128, hi as i128), + AxisRangeBounds::Sum { lo, hi } => (lo as i128, hi as i128), + AxisRangeBounds::Avg { lo, hi } => (lo, hi), } - query } } @@ -253,8 +256,9 @@ pub struct DocumentHavingMode { /// A resolved having-range query. Shared by the prover and the verifier — /// both build the grove path through /// [`DriveDocumentHavingQuery::indexed_property_name_tree_path`] and the -/// secondary query through [`AxisRangeBounds::merk_query`], so the two -/// cannot drift on which subtree or which range the proof is about. +/// same bounded axis `PathQuery` through +/// [`AxisRangeBounds::i128_bounds`], so the two cannot drift on which +/// subtree or which range the proof is about. #[derive(Debug, Clone)] #[cfg(any(feature = "server", feature = "verify"))] pub struct DriveDocumentHavingQuery<'a> { diff --git a/packages/rs-drive/src/query/drive_document_having_query/tests.rs b/packages/rs-drive/src/query/drive_document_having_query/tests.rs index 9bfd3277323..702fb5d7277 100644 --- a/packages/rs-drive/src/query/drive_document_having_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_having_query/tests.rs @@ -557,10 +557,27 @@ mod bounds { } #[test] - fn merk_query_direction_follows_descending() { - let bounds = AxisRangeBounds::Count { lo: 101, hi: 200 }; - assert!(bounds.merk_query(false).left_to_right); - assert!(!bounds.merk_query(true).left_to_right); + fn i128_bounds_widen_every_axis_losslessly() { + assert_eq!( + AxisRangeBounds::Count { + lo: 101, + hi: u64::MAX + } + .i128_bounds(), + (101, u64::MAX as i128) + ); + assert_eq!( + AxisRangeBounds::Sum { + lo: i64::MIN, + hi: -5 + } + .i128_bounds(), + (i64::MIN as i128, -5) + ); + assert_eq!( + AxisRangeBounds::Avg { lo: -5, hi: 5 }.i128_bounds(), + (-5, 5) + ); } } @@ -759,6 +776,7 @@ mod execution { having: &having, order_by: &order_by, where_clauses: &[], + resolved_time_ranges: &[], limit: case.limit, offset: None, has_start_at: false, @@ -1048,24 +1066,19 @@ mod execution { let (drive, contract) = setup_restaurants(); // Empty secondary (no documents at all): the unproven read - // returns the empty list, but grovedb's range prover — unlike - // the ranked surface's paginated prover — has no absence-proof - // shape for a completely empty tree and refuses. drive-abci - // maps this exact failure class onto an `InvalidArgument` - // telling the caller to retry unproved - // (`empty_ranking_proof_rejection`); at the drive level it - // surfaces as the grovedb error asserted here. If a future - // grovedb pin makes empty range proofs work, this arm should - // flip to a round-trip assertion. + // returns the empty list, and the unified bounded-axis prover + // proves it — an empty secondary is carried as empty proof + // bytes that the verifier resolves to a NULL_HASH secondary + // root, so the parent binding only passes when the element + // genuinely commits an empty secondary. (The old standalone + // range prover refused this state outright with "Cannot create + // proof for empty tree", which is why drive-abci still keeps + // `empty_ranking_proof_rejection` as a backstop for that error + // class.) let case = HavingCase::count(HavingOperator::GreaterThan, Value::U64(100), 10); let entries = entries_of(run(&drive, &contract, &case, false).expect("read succeeds")); assert!(entries.is_empty()); - let error = run(&drive, &contract, &case, true) - .expect_err("proving against an empty secondary is refused by grovedb"); - assert!( - format!("{error}").contains("Cannot create proof for empty tree"), - "the failure must be the recognized empty-tree class, got: {error}" - ); + assert_proof_round_trips(&drive, &contract, &case, &entries); // Populated secondary, bound above every count: a genuine // absence proof, which works — the tree has content to anchor @@ -1094,6 +1107,14 @@ mod execution { /// verify under both — correctly, because the range boundaries /// prove both claims — so the distinguishing group is the point of /// the fixture. + /// + /// The unified verifier is query-as-input rather than echo-checked: + /// the invariant it holds is "a proof can never make the client + /// believe a wrong answer to the client's own query", not "a proof + /// only verifies under the exact request it was built for". A + /// tampered query whose answer the proof also correctly attests + /// (the limit case at the end) therefore verifies — to that + /// query's own correct answer. #[test] fn a_proof_does_not_verify_under_different_bounds() { let (drive, contract) = setup_restaurants(); @@ -1138,18 +1159,27 @@ mod execution { "a proof of `> 2` must not verify as `> 1`" ); - // Nor under a different direction or limit. + // Nor under a different direction. tampered_query = client_side_query(&contract, &over_two); tampered_query.descending = true; assert!(tampered_query .verify_having_range_proof(&proof, platform_version()) .is_err()); + // A different limit is NOT an echo check under the unified + // query-as-input verifier: the same bytes verify under any limit + // they can correctly answer. Here exactly one group matches + // `> 2`, so limit 5 and limit 10 have the same answer and the + // proof verifies to it — the sound outcome, since the entries + // ARE the right answer to the limit-5 question. What can never + // happen is an over-long answer: the verifier's own shape check + // caps entries at the query's limit. tampered_query = client_side_query(&contract, &over_two); tampered_query.limit = 5; - assert!(tampered_query + let (_, entries) = tampered_query .verify_having_range_proof(&proof, platform_version()) - .is_err()); + .expect("a proof whose answer also answers the smaller limit verifies under it"); + assert_eq!(keys_of(&entries), vec!["beta"]); } /// A `having` on an axis no index declares is refused with the @@ -1379,6 +1409,7 @@ mod identifier_group_keys { having: &having, order_by, where_clauses: &[], + resolved_time_ranges: &[], limit: Some(10), offset: None, has_start_at: false, @@ -1672,6 +1703,7 @@ mod pinned_prefix { having: &having, order_by, where_clauses, + resolved_time_ranges: &[], limit: Some(10), offset: None, has_start_at: false, @@ -2030,6 +2062,7 @@ mod pinned_prefix { having: &having, order_by: &[], where_clauses: &null_pin, + resolved_time_ranges: &[], limit: Some(10), offset: None, has_start_at: 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 index 23bd902fe6e..912f4ab4a70 100644 --- 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 @@ -17,9 +17,11 @@ use super::mode_detection::detect_ranked_mode; use super::{RankedPage, RankedPaginationInputs}; use crate::drive::Drive; +use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::having::HavingClause; use crate::query::projection::SelectProjection; +use crate::query::ResolvedTimeRange; use crate::query::{OrderClause, WhereClause}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; @@ -64,6 +66,16 @@ pub struct DocumentRankedRequest<'a> { /// equality pins on the covering compound index's leading /// properties for the pinned-prefix form. pub where_clauses: &'a [WhereClause], + /// The fields among `where_clauses` whose equality clause was produced by + /// `IN_TIME_RANGE` resolution (see + /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]). + /// Must be empty: `where_clauses` must be empty, so there is nothing to + /// have resolved, and ranking over bucket keys is undesigned — a document + /// belongs to `overlap_factor` buckets at once, so it would contribute to + /// that many groups. Carried (and rejected) here for the same reason + /// `where_clauses` is: drive owns the rejection regardless of which + /// upstream path built the request. + pub resolved_time_ranges: &'a [ResolvedTimeRange], /// Request `limit` — the ranking's `k`. **Required**; there is no /// server default a verifying client could reproduce. pub limit: Option, @@ -128,6 +140,19 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { + // Unreachable behind the empty-`where_clauses` rule `detect_ranked_mode` + // enforces below — a resolved equality is a where clause — but stated + // here so the ranked surface's exclusion of bucketed indexes is a + // rejection rather than a silent fallback to another index. + if !request.resolved_time_ranges.is_empty() { + return Err(Error::Query(QuerySyntaxError::Unsupported( + "a ranked query cannot carry a time-range (IN_TIME_RANGE) selection: ranking \ + groups by an index's only property, and a document belongs to every bucket \ + that contains its timestamp, so it would be ranked into several groups at once" + .to_string(), + ))); + } + let mode = detect_ranked_mode( &request.select, request.group_by, 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 index 9cda62c9234..e24b07b3aa8 100644 --- 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 @@ -15,8 +15,10 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use dpp::version::PlatformVersion; -use grovedb::{IndexedTopKKeysPage, TransactionArg}; +use grovedb::query_result_type::QueryResultType; +use grovedb::{AxisKeys, PathQuery, PathQueryRun, TransactionArg}; use grovedb_costs::CostContext; +use grovedb_query::AxisQuery; impl DriveDocumentRankedQuery<'_> { /// Read one page of the ranking directly from the axis secondary: @@ -73,8 +75,20 @@ impl DriveDocumentRankedQuery<'_> { ) -> 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; + + // The same axis PathQuery the prove path uses, with the + // keys-only projection: the ranking pairs are read straight off + // the pinned secondary view, no primary values resolved. + let path_query = PathQuery::new_axis( + path, + AxisQuery::top_k( + self.axis.into(), + self.k, + self.offset as u64, + self.descending, + ) + .keys_only(), + ); // The cost is dropped rather than `.unwrap()`-ed: // `CostContext::unwrap` is infallible (it drops the cost field) @@ -84,73 +98,57 @@ impl DriveDocumentRankedQuery<'_> { // above it accumulates or charges the cost, and no credit is // debited for a read. grovedb computes the `OperationCost` // because its API always does, and it ends here. - let (entries, skipped) = match self.axis { - RankedAxis::Count => { - let CostContext { value, cost: _ } = - drive.grove.indexed_count_top_k_paginated_keys( - path_refs.as_slice(), - self.k, - offset, - self.descending, - transaction, - grove_version, - ); - let IndexedTopKKeysPage { entries, skipped } = - value.map_err(|e| Error::GroveDB(Box::new(e)))?; - ( - entries - .into_iter() - .map(|(count, key)| RankedEntry { - key, - value: RankedEntryValue::Count(count), - }) - .collect::>(), - skipped, - ) - } - RankedAxis::Sum => { - let CostContext { value, cost: _ } = drive.grove.indexed_sum_top_k_paginated_keys( - path_refs.as_slice(), - self.k, - offset, - self.descending, - transaction, - grove_version, - ); - let IndexedTopKKeysPage { entries, skipped } = - value.map_err(|e| Error::GroveDB(Box::new(e)))?; - ( - entries - .into_iter() - .map(|(sum, key)| RankedEntry { - key, - value: RankedEntryValue::Sum(sum), - }) - .collect::>(), - skipped, - ) - } - RankedAxis::Avg => { - let CostContext { value, cost: _ } = drive.grove.indexed_avg_top_k_paginated_keys( - path_refs.as_slice(), - self.k, - offset, - self.descending, - transaction, - grove_version, - ); - let IndexedTopKKeysPage { entries, skipped } = - value.map_err(|e| Error::GroveDB(Box::new(e)))?; - ( - entries - .into_iter() - .map(|(avg, key)| RankedEntry { - key, - value: RankedEntryValue::AvgFixedPoint(avg), - }) - .collect::>(), - skipped, - ) + let CostContext { value, cost: _ } = drive.grove.run_path_query( + &path_query, + true, + true, + true, + QueryResultType::QueryPathKeyElementTrioResultType, + transaction, + grove_version, + ); + let PathQueryRun::AxisKeys { keys, skipped } = + value.map_err(|e| Error::GroveDB(Box::new(e)))? + else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked {:?} read ran to a non-axis-keys result shape", + self.axis + )))); + }; + let skipped = skipped.ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked {:?} read carried no skip count for a paginated walk", + self.axis + ))) + })?; + + let entries = match (self.axis, keys) { + (RankedAxis::Count, AxisKeys::Count(pairs)) => pairs + .into_iter() + .map(|(count, key)| RankedEntry { + key, + value: RankedEntryValue::Count(count), + }) + .collect::>(), + (RankedAxis::Sum, AxisKeys::Sum(pairs)) => pairs + .into_iter() + .map(|(sum, key)| RankedEntry { + key, + value: RankedEntryValue::Sum(sum), + }) + .collect::>(), + (RankedAxis::Avg, AxisKeys::Avg(pairs)) => pairs + .into_iter() + .map(|(avg, key)| RankedEntry { + key, + value: RankedEntryValue::AvgFixedPoint(avg), + }) + .collect::>(), + (axis, other) => { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked {axis:?} read returned {} pairs of a different axis shape", + other.len() + )))); } }; @@ -170,21 +168,25 @@ impl DriveDocumentRankedQuery<'_> { Ok(RankedPage { skipped, entries }) } - /// Generate the grovedb indexed-axis paginated top-k proof for this - /// query. + /// Generate the axis-ordered top-k proof for this query, through the + /// unified `PathQuery` surface (grovedb's only public proof surface + /// for indexed-axis reads): the query is + /// [`PathQuery::new_axis_top_k`] and the envelope is a GroveDBProof + /// V1 carrying an axis descent into the queried secondary. /// /// 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 + /// sibling axes' root hashes, and the ordinary layer chain up to the + /// grovedb root — so the client reconstructs the platform root hash + /// from it. `(axis, k, offset, descending)` are **not echoed** in the + /// envelope: the verifier takes the client's own reconstruction of + /// the same `PathQuery` as input, so a proof generated for a + /// different ranking — or a different page — fails verification + /// rather than being silently reinterpreted. That is why `k` is /// validated rather than clamped upstream (a clamped `k` would - /// produce a proof the client's own reconstruction rejects). + /// produce a proof the client's own query rejects). /// - /// The paginated primitive is used unconditionally, with + /// The paginated traversal 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. @@ -204,23 +206,27 @@ impl DriveDocumentRankedQuery<'_> { pub fn execute_top_k_with_proof( &self, drive: &Drive, - transaction: TransactionArg, + _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(), + let path_query = PathQuery::new_axis_top_k( + path, self.axis.into(), self.k, self.offset as u64, self.descending, - transaction, - grove_version, ); + + // The unified prover proves committed state — it takes no + // transaction. The parameter is kept for signature stability with + // the no-proof executor; the query dispatch passes `None` on this + // surface anyway (queries answer from committed state). + // + // Same destructure-don't-unwrap rationale as the no-proof arm. + let CostContext { value, cost: _ } = + drive.grove.prove_query(&path_query, None, grove_version); value.map_err(|e| Error::GroveDB(Box::new(e))) } } 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 index 5f5408fbf30..86fdaf4414b 100644 --- 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 @@ -36,7 +36,8 @@ use std::collections::BTreeMap; /// 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. +/// index accumulates would silently answer about the wrong property; +/// - it carries no time-range transform. /// /// With no pins this degenerates to the original single-property rule. /// A partial pin (some but not all leading properties) matches nothing — @@ -85,6 +86,14 @@ pub fn find_ranked_index_for_axis<'b>( { return false; } + // Ranking over bucket keys is an undesigned surface: a document is + // stored once per bucket that contains it, so it would contribute to + // `overlap_factor` groups at once, and a ranked query carries no + // where clauses that could pin a single bucket. Exclude bucketed + // indexes until the semantics are deliberately designed. + if index.time_range.is_some() { + return false; + } match axis { RankedAxis::Count => index.ranked_countable, RankedAxis::Sum => { 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 index a72ccd7b7f4..68a52700540 100644 --- a/packages/rs-drive/src/query/drive_document_ranked_query/mod.rs +++ b/packages/rs-drive/src/query/drive_document_ranked_query/mod.rs @@ -117,10 +117,10 @@ mod tests; /// 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. +/// because the proof is verified against the client's own +/// reconstruction of the same axis `PathQuery` (with the client's `k`) +/// by [`grovedb::GroveDb::verify_path_query`] — a server-side clamp +/// would produce a proof the client's own query rejects. /// /// There is deliberately **no companion ceiling on `OFFSET`**; see the /// module docs and [`DriveDocumentRankedQuery::offset`]. 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 index 0052ab8388e..5298bd31cb3 100644 --- a/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs @@ -690,6 +690,7 @@ fn test_index(name: &str, properties: &[&str], summable: Option<&str>) -> Index ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } @@ -961,6 +962,7 @@ fn run( offset: case.offset, has_start_at: false, prove, + resolved_time_ranges: &[], }, None, platform_version(), @@ -2247,6 +2249,7 @@ mod pinned_prefix { offset: None, has_start_at: false, prove, + resolved_time_ranges: &[], }, None, platform_version(), @@ -2464,6 +2467,7 @@ mod pinned_prefix { offset: None, has_start_at: false, prove, + resolved_time_ranges: &[], }; let page = match drive diff --git a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs index 11d3ef38db8..da3cf1652ef 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs @@ -10,13 +10,18 @@ //! CBOR-decoded `Value::Array` input into structured `Vec` / //! `Vec`. Identical input contract to count. +use crate::config::DriveConfig; use crate::drive::Drive; +use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::drive_document_sum_query::{ DocumentSumMode, DocumentSumRequest, DocumentSumResponse, RangeSumOptions, RangeSumWalkMode, SumMode, }; -use crate::query::{OrderClause, WhereClause}; +use crate::query::{ + validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes, + OrderClause, WhereClause, +}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::platform_value::Value; @@ -25,18 +30,16 @@ use grovedb::TransactionArg; fn effective_no_proof_distinct_limit( requested_limit: Option, - drive_config: &crate::config::DriveConfig, + drive_config: &DriveConfig, ) -> Result { let effective_limit = requested_limit .unwrap_or(drive_config.default_query_limit as u32) .min(drive_config.max_query_limit as u32); if effective_limit == 0 { - return Err(Error::Query( - crate::error::query::QuerySyntaxError::InvalidLimit( - "effective distinct SUM limit must be greater than zero".to_string(), - ), - )); + return Err(Error::Query(QuerySyntaxError::InvalidLimit( + "effective distinct SUM limit must be greater than zero".to_string(), + ))); } // Both configuration limits are u16, and the `min` above bounds every @@ -53,15 +56,28 @@ impl Drive { /// Mirrors [`Drive::execute_document_count_request`]. pub fn execute_document_sum_request( &self, - request: DocumentSumRequest, + mut request: DocumentSumRequest, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { + // Canonicalize exactly as the count and joint dispatchers do (the + // shared step in [`crate::query::canonicalize`]), so callers can + // pass either the bounded pair form (`[f > A, f < B]`) or the + // pre-merged `between*` form and get equivalent mode detection. + request.where_clauses = + validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; + // Same provenance-vs-shape contract as the count and joint + // dispatchers, anchored before mode detection just as there. + validate_resolved_time_range_clause_shapes( + &request.where_clauses, + &request.resolved_time_ranges, + )?; let resolved_mode = super::mode_detection::detect_sum_mode(&request, platform_version)?; let contract_id = request.contract.id().to_buffer(); let document_type_name = request.document_type.name().to_string(); let where_clauses = request.where_clauses; + let resolved_time_ranges = request.resolved_time_ranges; let sum_property = request.sum_property; // Default direction is ascending; the first order clause's // direction (if any) wins. Mirrors count's analog. @@ -78,6 +94,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, transaction, platform_version, @@ -97,6 +114,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, options, transaction, @@ -127,6 +145,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, options, transaction, @@ -145,6 +164,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, transaction, platform_version, @@ -196,6 +216,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, limit_u16, order_by_ascending, @@ -210,6 +231,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, transaction, platform_version, @@ -254,6 +276,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_ranges, sum_property, limit_u16, order_by_ascending, diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/per_in_value.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/per_in_value.rs index 251cf2917f8..e54dedd6187 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/per_in_value.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/per_in_value.rs @@ -11,6 +11,7 @@ use super::super::{DriveDocumentSumQuery, RangeSumOptions, SumEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::{WhereClause, WhereOperator}; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -29,6 +30,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, options: RangeSumOptions, transaction: TransactionArg, @@ -82,6 +84,7 @@ impl Drive { document_type.indexes(), &clauses_for_value, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/point_lookup_proof.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/point_lookup_proof.rs index 0c65b144623..2de39655fa6 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/point_lookup_proof.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/point_lookup_proof.rs @@ -6,6 +6,7 @@ use super::super::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -25,6 +26,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -59,6 +61,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_aggregate_carrier_proof.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_aggregate_carrier_proof.rs index 67cde266e34..305348b28a4 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_aggregate_carrier_proof.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_aggregate_carrier_proof.rs @@ -15,6 +15,7 @@ use super::super::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -45,6 +46,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, limit: Option, left_to_right: bool, @@ -55,6 +57,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_distinct_proof.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_distinct_proof.rs index e06555e1c7f..d448b7a8f83 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_distinct_proof.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_distinct_proof.rs @@ -7,6 +7,7 @@ use super::super::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -26,6 +27,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, limit: u16, left_to_right: bool, @@ -36,6 +38,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs index be1ad9c44f7..deffdc15c58 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs @@ -6,6 +6,7 @@ use super::super::{DriveDocumentSumQuery, RangeSumOptions, SumEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -23,6 +24,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, options: RangeSumOptions, transaction: TransactionArg, @@ -32,6 +34,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_proof.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_proof.rs index ef2b0e843f2..b166203ea8e 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_proof.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_proof.rs @@ -8,6 +8,7 @@ use super::super::DriveDocumentSumQuery; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -25,6 +26,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -33,6 +35,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/total.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/total.rs index 600868a045d..c872b803b38 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/total.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/total.rs @@ -10,6 +10,7 @@ use super::super::{DriveDocumentSumQuery, SumEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; +use crate::query::ResolvedTimeRange; use crate::query::WhereClause; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::version::PlatformVersion; @@ -30,6 +31,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_ranges: &[ResolvedTimeRange], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -65,6 +67,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( diff --git a/packages/rs-drive/src/query/drive_document_sum_query/index_picker.rs b/packages/rs-drive/src/query/drive_document_sum_query/index_picker.rs index 61f425b06fb..0d3c5251e7f 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/index_picker.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/index_picker.rs @@ -15,7 +15,8 @@ //! prover and verifier reject the same set of inputs (same as count). use crate::query::drive_document_sum_query::{is_indexable_for_sum, is_range_operator}; -use crate::query::{WhereClause, WhereOperator}; +use crate::query::ResolvedTimeRange; +use crate::query::{index_admissible_for_resolved_time_range, WhereClause, WhereOperator}; use dpp::data_contract::document_type::Index; use std::collections::{BTreeMap, BTreeSet}; @@ -26,10 +27,16 @@ use std::collections::{BTreeMap, BTreeSet}; /// Mirror of count's `find_countable_index_for_where_clauses` with the /// additional `summable == Some(sum_property)` predicate on top of the /// strict-coverage match. +/// +/// `resolved_time_ranges` names the fields whose equality clause was +/// produced by `IN_TIME_RANGE` resolution (see +/// [`crate::query::resolve_time_range_bucket_clause`]) and gates which indexes +/// are candidates — see [`index_admissible_for_resolved_time_range`]. pub fn find_summable_index_for_where_clauses<'b>( indexes: &'b BTreeMap, where_clauses: &[WhereClause], sum_property: &str, + resolved_time_ranges: &[ResolvedTimeRange], ) -> Option<&'b Index> { // Defense-in-depth: any non-indexable operator immediately disqualifies // — the sum point-lookup path can only serve Equal/In. @@ -51,6 +58,14 @@ pub fn find_summable_index_for_where_clauses<'b>( } for index in indexes.values() { + // A time-range index holds one entry per bucket containing the + // document, keyed by bucket start: summing over it double-counts + // every document unless the query pins a single bucket, and only a + // resolution-produced equality does that. Conversely a raw clause + // must never bind to bucket keys. + if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) { + continue; + } // Skip if not summable OR if summable property doesn't match. match &index.summable { Some(prop) if prop == sum_property => {} @@ -77,10 +92,19 @@ pub fn find_summable_index_for_where_clauses<'b>( /// `sum_property`. /// /// Mirror of count's `find_range_countable_index_for_where_clauses`. +/// +/// `resolved_time_ranges` gates the candidate set exactly as in +/// [`find_summable_index_for_where_clauses`]. A resolved field never arrives +/// as a range clause — resolution always produces an equality — so with a +/// non-empty list the only bucketed index this can return is one whose +/// resolved equality is a prefix property and whose range terminator is a +/// different property. That is the intended shape: a range over one property +/// within a single time bucket. pub fn find_range_summable_index_for_where_clauses<'b>( indexes: &'b BTreeMap, where_clauses: &[WhereClause], sum_property: &str, + resolved_time_ranges: &[ResolvedTimeRange], ) -> Option<&'b Index> { let range_clauses: Vec<&WhereClause> = where_clauses .iter() @@ -121,6 +145,13 @@ pub fn find_range_summable_index_for_where_clauses<'b>( .collect(); for index in indexes.values() { + // Same admissibility rule as the point-lookup picker: bucketed + // indexes store one entry per containing bucket, so only a query + // pinned to a single bucket by a resolution-produced equality may + // walk them, and raw clauses may never bind to bucket keys. + if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) { + continue; + } if !index.range_summable { continue; } diff --git a/packages/rs-drive/src/query/drive_document_sum_query/mod.rs b/packages/rs-drive/src/query/drive_document_sum_query/mod.rs index c1fb5d6fe6b..50a544cbdde 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/mod.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/mod.rs @@ -53,6 +53,8 @@ pub mod executors; #[cfg(test)] mod tests; +#[cfg(feature = "server")] +use crate::query::ResolvedTimeRange; #[cfg(any(feature = "server", feature = "verify"))] use crate::query::{WhereClause, WhereOperator}; @@ -180,6 +182,12 @@ pub struct DocumentSumRequest<'a> { /// [`drive_dispatcher::where_clauses_from_value`] from the /// wire-CBOR shape). pub where_clauses: Vec, + /// The fields among `where_clauses` whose equality clause was produced by + /// `IN_TIME_RANGE` resolution rather than written by the caller. Same + /// contract and same purpose as + /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]: + /// it is what gates which indexes the sum pickers may select. + pub resolved_time_ranges: Vec, /// Structured order-clauses (parsed via /// [`drive_dispatcher::order_clauses_from_value`]). pub order_clauses: Vec, diff --git a/packages/rs-drive/src/query/drive_document_sum_query/path_query.rs b/packages/rs-drive/src/query/drive_document_sum_query/path_query.rs index 3af9b4540cc..39a1ff70b3f 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/path_query.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/path_query.rs @@ -22,6 +22,7 @@ use crate::drive::RootTree; use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::drive_document_sum_query::{is_range_operator, DriveDocumentSumQuery}; +use crate::query::ResolvedTimeRange; use crate::query::{WhereClause, WhereOperator}; // `serialize_value_for_key` is a `DocumentTypeV0Methods` method, NOT // `DocumentTypeBasicMethods` (which is the trait of versionless basic @@ -112,10 +113,11 @@ impl<'a> DriveDocumentSumQuery<'a> { platform_version, )?; if in_outer_keys.is_some() { - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension + .push(self.index.level_key_for_property(&prop.name).into_bytes()); subquery_path_extension.push(serialized); } else { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); base_path.push(serialized); } } @@ -128,7 +130,7 @@ impl<'a> DriveDocumentSumQuery<'a> { ), )); } - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); let in_values = clause.in_values().into_data_with_error()??; let mut keys: Vec> = in_values .iter() @@ -285,7 +287,7 @@ impl<'a> DriveDocumentSumQuery<'a> { ), )); } - path.push(prop.name.as_bytes().to_vec()); + path.push(self.index.level_key_for_property(&prop.name).into_bytes()); path.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -302,7 +304,11 @@ impl<'a> DriveDocumentSumQuery<'a> { ), ))? .name; - path.push(range_prop_name.as_bytes().to_vec()); + path.push( + self.index + .level_key_for_property(range_prop_name) + .into_bytes(), + ); // grovedb PR 670 surface: `Query::new_aggregate_sum_on_range`. let query = Query::new_aggregate_sum_on_range(query_item); @@ -371,7 +377,7 @@ impl<'a> DriveDocumentSumQuery<'a> { ), )); } - path.push(prop.name.as_bytes().to_vec()); + path.push(self.index.level_key_for_property(&prop.name).into_bytes()); path.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -388,7 +394,11 @@ impl<'a> DriveDocumentSumQuery<'a> { ), ))? .name; - path.push(range_prop_name.as_bytes().to_vec()); + path.push( + self.index + .level_key_for_property(range_prop_name) + .into_bytes(), + ); let query = grovedb::Query::new_aggregate_count_and_sum_on_range(query_item); Ok(PathQuery::new( @@ -602,10 +612,11 @@ impl<'a> DriveDocumentSumQuery<'a> { platform_version, )?; if in_outer_keys.is_some() { - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension + .push(self.index.level_key_for_property(&prop.name).into_bytes()); subquery_path_extension.push(serialized); } else { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); base_path.push(serialized); } } @@ -620,7 +631,7 @@ impl<'a> DriveDocumentSumQuery<'a> { } // Path stops at the In-bearing prop's property- // name subtree; outer Query lives at that level. - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); let in_values = clause.in_values().into_data_with_error()??; let mut keys: Vec> = in_values .iter() @@ -813,7 +824,7 @@ impl<'a> DriveDocumentSumQuery<'a> { ))?; match (&carrier, clause.operator) { (Carrier::Pending, WhereOperator::Equal) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); base_path.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -821,15 +832,16 @@ impl<'a> DriveDocumentSumQuery<'a> { )?); } (Carrier::Pending, WhereOperator::In) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); carrier = Carrier::In(clause.clone()); } (Carrier::Pending, op) if is_range_operator(op) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); carrier = Carrier::Range(clause.clone()); } (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => { - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension + .push(self.index.level_key_for_property(&prop.name).into_bytes()); subquery_path_extension.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -854,7 +866,11 @@ impl<'a> DriveDocumentSumQuery<'a> { } } } - subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec()); + subquery_path_extension.push( + self.index + .level_key_for_property(terminator_prop_name) + .into_bytes(), + ); let mut outer_query = Query::new_with_direction(left_to_right); match carrier { @@ -1001,7 +1017,7 @@ impl<'a> DriveDocumentSumQuery<'a> { ))?; match (&carrier, clause.operator) { (Carrier::Pending, WhereOperator::Equal) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); base_path.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -1009,15 +1025,16 @@ impl<'a> DriveDocumentSumQuery<'a> { )?); } (Carrier::Pending, WhereOperator::In) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); carrier = Carrier::In(clause.clone()); } (Carrier::Pending, op) if is_range_operator(op) => { - base_path.push(prop.name.as_bytes().to_vec()); + base_path.push(self.index.level_key_for_property(&prop.name).into_bytes()); carrier = Carrier::Range(clause.clone()); } (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => { - subquery_path_extension.push(prop.name.as_bytes().to_vec()); + subquery_path_extension + .push(self.index.level_key_for_property(&prop.name).into_bytes()); subquery_path_extension.push(self.document_type.serialize_value_for_key( prop.name.as_str(), &clause.value, @@ -1043,7 +1060,11 @@ impl<'a> DriveDocumentSumQuery<'a> { } } } - subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec()); + subquery_path_extension.push( + self.index + .level_key_for_property(terminator_prop_name) + .into_bytes(), + ); let mut outer_query = Query::new_with_direction(left_to_right); match carrier { @@ -1107,6 +1128,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type: DocumentTypeRef, sum_property: &str, where_clauses: &[WhereClause], + resolved_time_ranges: &[ResolvedTimeRange], platform_version: &PlatformVersion, ) -> Result { use crate::query::drive_document_sum_query::index_picker::find_summable_index_for_where_clauses; @@ -1117,6 +1139,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type.indexes(), where_clauses, sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( @@ -1145,6 +1168,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type: DocumentTypeRef, sum_property: &str, where_clauses: &[WhereClause], + resolved_time_ranges: &[ResolvedTimeRange], platform_version: &PlatformVersion, ) -> Result { use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses; @@ -1155,6 +1179,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type.indexes(), where_clauses, sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( @@ -1183,11 +1208,13 @@ impl<'a> DriveDocumentSumQuery<'a> { /// Used by the SDK verifier-side rebuild via /// `GroveDb::verify_aggregate_sum_query_per_key` (grovedb PR #670 /// head `e98bab5f`). + #[allow(clippy::too_many_arguments)] pub fn carrier_aggregate_sum_path_query_static( contract: &DataContract, document_type: DocumentTypeRef, sum_property: &str, where_clauses: &[WhereClause], + resolved_time_ranges: &[ResolvedTimeRange], limit: Option, left_to_right: bool, platform_version: &PlatformVersion, @@ -1200,6 +1227,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type.indexes(), where_clauses, sum_property, + resolved_time_ranges, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( 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 aa1d0566be1..2aed0b93f20 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 @@ -45,6 +45,7 @@ fn summable_index(name: &str, props: &[&str], summable: Option<&str>) -> Index { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } @@ -64,6 +65,7 @@ fn range_summable_index(name: &str, props: &[&str], summable: &str) -> Index { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } @@ -104,7 +106,8 @@ fn summable_picker_matches_single_prop_exactly() { &["recipient"], Some("amount"), )]); - let found = find_summable_index_for_where_clauses(&indexes, &[wc_equal("recipient")], "amount"); + let found = + find_summable_index_for_where_clauses(&indexes, &[wc_equal("recipient")], "amount", &[]); assert_eq!(found.map(|i| i.name.as_str()), Some("byRecipient")); } @@ -113,7 +116,7 @@ fn summable_picker_rejects_partial_coverage() { // Two-prop index with only one of the props matched by where clauses. let indexes = make_index_map(vec![summable_index("byAB", &["a", "b"], Some("amount"))]); assert!( - find_summable_index_for_where_clauses(&indexes, &[wc_equal("a")], "amount").is_none(), + find_summable_index_for_where_clauses(&indexes, &[wc_equal("a")], "amount", &[]).is_none(), "partial coverage must miss the strict picker" ); } @@ -127,7 +130,8 @@ fn summable_picker_rejects_property_mismatch() { Some("amount"), )]); assert!( - find_summable_index_for_where_clauses(&indexes, &[wc_equal("recipient")], "fee").is_none() + find_summable_index_for_where_clauses(&indexes, &[wc_equal("recipient")], "fee", &[]) + .is_none() ); } @@ -135,10 +139,13 @@ fn summable_picker_rejects_property_mismatch() { fn summable_picker_rejects_non_summable_index() { // No `summable` declaration → never picked, even if properties match. let indexes = make_index_map(vec![summable_index("byRecipient", &["recipient"], None)]); - assert!( - find_summable_index_for_where_clauses(&indexes, &[wc_equal("recipient")], "amount") - .is_none() - ); + assert!(find_summable_index_for_where_clauses( + &indexes, + &[wc_equal("recipient")], + "amount", + &[] + ) + .is_none()); } #[test] @@ -149,7 +156,8 @@ fn summable_picker_rejects_range_operator() { Some("amount"), )]); assert!( - find_summable_index_for_where_clauses(&indexes, &[wc_gt("sentAt", 0)], "amount").is_none(), + find_summable_index_for_where_clauses(&indexes, &[wc_gt("sentAt", 0)], "amount", &[]) + .is_none(), "any range operator disqualifies the point-lookup picker" ); } @@ -161,7 +169,8 @@ fn summable_picker_accepts_in_clause() { &["recipient"], Some("amount"), )]); - let found = find_summable_index_for_where_clauses(&indexes, &[wc_in("recipient")], "amount"); + let found = + find_summable_index_for_where_clauses(&indexes, &[wc_in("recipient")], "amount", &[]); assert_eq!(found.map(|i| i.name.as_str()), Some("byRecipient")); } @@ -177,7 +186,7 @@ fn range_summable_picker_matches_terminator_range() { "amount", )]); let found = - find_range_summable_index_for_where_clauses(&indexes, &[wc_gt("sentAt", 0)], "amount"); + find_range_summable_index_for_where_clauses(&indexes, &[wc_gt("sentAt", 0)], "amount", &[]); assert_eq!(found.map(|i| i.name.as_str()), Some("bySentAt")); } @@ -191,7 +200,8 @@ fn range_summable_picker_matches_prefix_equal_plus_terminator_range() { "amount", )]); let where_clauses = vec![wc_equal("recipient"), wc_gt("sentAt", 0)]; - let found = find_range_summable_index_for_where_clauses(&indexes, &where_clauses, "amount"); + let found = + find_range_summable_index_for_where_clauses(&indexes, &where_clauses, "amount", &[]); assert_eq!(found.map(|i| i.name.as_str()), Some("byRecipientTime")); } @@ -203,10 +213,13 @@ fn range_summable_picker_rejects_property_mismatch() { &["sentAt"], "amount", )]); - assert!( - find_range_summable_index_for_where_clauses(&indexes, &[wc_gt("sentAt", 0)], "fee") - .is_none() - ); + assert!(find_range_summable_index_for_where_clauses( + &indexes, + &[wc_gt("sentAt", 0)], + "fee", + &[] + ) + .is_none()); } #[test] @@ -216,10 +229,13 @@ fn range_summable_picker_rejects_non_range_summable() { let mut idx = range_summable_index("bySentAt", &["sentAt"], "amount"); idx.range_summable = false; let indexes = make_index_map(vec![idx]); - assert!( - find_range_summable_index_for_where_clauses(&indexes, &[wc_gt("sentAt", 0)], "amount") - .is_none() - ); + assert!(find_range_summable_index_for_where_clauses( + &indexes, + &[wc_gt("sentAt", 0)], + "amount", + &[] + ) + .is_none()); } #[test] @@ -233,7 +249,8 @@ fn range_summable_picker_rejects_range_not_on_terminator() { )]); let where_clauses = vec![wc_gt("recipient", 0)]; assert!( - find_range_summable_index_for_where_clauses(&indexes, &where_clauses, "amount").is_none() + find_range_summable_index_for_where_clauses(&indexes, &where_clauses, "amount", &[]) + .is_none() ); } @@ -452,6 +469,7 @@ mod limit_policy_regression { limit: None, prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let response = drive @@ -472,6 +490,7 @@ mod limit_policy_regression { document_type.indexes(), std::slice::from_ref(&color_gt_blue), "amount", + &[], ) .expect("byColor rangeSummable index covers `color > blue`"); let sum_query = DriveDocumentSumQuery { @@ -548,6 +567,7 @@ mod limit_policy_regression { limit: Some(over_max), prove: true, drive_config: &drive_config, + resolved_time_ranges: vec![], }; let err = drive @@ -619,6 +639,7 @@ mod limit_policy_regression { limit, prove: false, drive_config: &drive_config, + resolved_time_ranges: vec![], }; for (requested, expected) in [(None, 2), (Some(1), 1), (Some(10_000), 3)] { diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index 97a7dfcb5e0..4907fb1eb64 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -1,3 +1,4 @@ +use dpp::data_contract::document_type::TimeRangeTransform; use std::sync::Arc; #[cfg(any(feature = "server", feature = "verify"))] @@ -142,6 +143,10 @@ use crate::util::grove_operations::QueryType::StatefulQuery; // Module declarations that are conditional on either "server" or "verify" features #[cfg(any(feature = "server", feature = "verify"))] +pub mod canonicalize; +#[cfg(any(feature = "server", feature = "verify"))] +pub use canonicalize::validate_and_canonicalize_where_clauses; +#[cfg(any(feature = "server", feature = "verify"))] pub mod conditions; #[cfg(any(feature = "server", feature = "verify"))] mod defaults; @@ -570,6 +575,292 @@ impl From for Vec { } } +/// Which active time range a `TOP(timeRange(...))` selection resolves to, +/// when the index's ranges overlap (`range > step`). Time-range queries are a +/// v1-only feature; the v0 query surface is unaffected. +#[cfg(any(feature = "server", feature = "verify"))] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +pub enum TimeRangeSelector { + /// The freshest started range (largest start ≤ now). Covers the latest + /// partial slice (0..step of history). + Newest, + /// The oldest range still active at now. Covers a near-full trailing + /// window of ~range of history. Best for "trending over the last window". + Oldest, +} + +#[cfg(any(feature = "server", feature = "verify"))] +impl TimeRangeSelector { + /// The selector's wire spelling — the `IN_TIME_RANGE` clause's operand on + /// the v1 `getDocuments` wire. The single source of truth for the string + /// form: the SDK encoder, the drive-abci decoder and the wasm-sdk JSON + /// parser all go through these two functions (and the serde derive above + /// is renamed to match), so the spellings cannot drift apart. + pub fn as_str(&self) -> &'static str { + match self { + TimeRangeSelector::Newest => "newest", + TimeRangeSelector::Oldest => "oldest", + } + } + + /// Parses the wire spelling. Returns `None` for anything but the exact + /// strings [`Self::as_str`] produces. + pub fn from_string(value: &str) -> Option { + match value { + "newest" => Some(TimeRangeSelector::Newest), + "oldest" => Some(TimeRangeSelector::Oldest), + _ => None, + } + } +} + +/// A concrete grid specification, matching a contract's `timeRange` +/// declaration verbatim (`range` / `step` / `phase`, in seconds). +/// +/// The structured `IN_TIME_RANGE` operand carries one of these when the +/// queried field is bucketed by more than one grid: the bare selector +/// (`"newest"` / `"oldest"`) is unambiguous only while exactly one time-range +/// index exists on the field, so a multi-grid field requires the query to +/// name the grid it wants. +#[cfg(any(feature = "server", feature = "verify"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TimeRangeGridSpec { + /// Window length in seconds, as the contract declares it. + pub range_seconds: u64, + /// Interval between window starts in seconds, as the contract declares it. + pub step_seconds: u64, + /// Grid alignment phase in seconds (0 when the contract omits `phase`). + pub phase_seconds: u64, +} + +#[cfg(any(feature = "server", feature = "verify"))] +impl TimeRangeGridSpec { + /// Whether this spec names exactly the given transform's grid. + pub fn matches(&self, transform: &TimeRangeTransform) -> bool { + self.range_seconds == transform.range_seconds + && self.step_seconds == transform.step_seconds + && self.phase_seconds == transform.phase_seconds + } +} + +/// Resolution provenance for one `IN_TIME_RANGE` clause: the field the +/// selector named and the exact grid the resolution used. Recorded by the +/// resolver's caller on the query (see +/// [`DriveDocumentQuery::resolved_time_ranges`]) and consumed by the index +/// pickers through [`index_admissible_for_resolved_time_range`], which pins +/// selection to the index carrying exactly this grid — a field may be +/// bucketed by several grids, so the field name alone no longer identifies +/// the index the resolution was computed against. +#[cfg(any(feature = "server", feature = "verify"))] +#[derive(Debug, Clone, PartialEq)] +pub struct ResolvedTimeRange { + /// The grid the bucket start was computed from. The transform carries its + /// own source field, so the provenance cannot name a field the grid does + /// not bucket — [`Self::field`] reads it from here. + pub transform: TimeRangeTransform, +} + +#[cfg(any(feature = "server", feature = "verify"))] +impl ResolvedTimeRange { + /// The bucketed source field the resolved equality is on — always the + /// transform's own source. + pub fn field(&self) -> &str { + &self.transform.source + } +} + +/// Resolves a time-range selection on `field` into a concrete equality +/// [`WhereClause`] on the bucketed source field, using the named grid's +/// `timeRange` transform and an authoritative `block_time_ms`. +/// +/// The server supplies `block_time_ms` from current block time and the +/// verifier re-derives it from the quorum-signed response metadata `time_ms`, +/// so both produce the identical concrete equality query — the existing +/// index/count proofs apply unchanged and the engine never needs a dedicated +/// time-range operator. +/// +/// `grid` selects among several time-range indexes on the same field: `None` +/// is accepted only while exactly one grid buckets the field (the common +/// case); with two or more grids the caller must name one, and naming a grid +/// no index declares is an error either way. +/// +/// What comes back is an ordinary equality clause, byte-identical to one a +/// client could have written by hand against a raw timestamp, plus the +/// [`ResolvedTimeRange`] provenance callers must record on the query (see +/// [`DriveDocumentQuery::resolved_time_ranges`]), which +/// [`DriveDocumentQuery::find_best_index`] and the aggregate index pickers +/// consume through [`index_admissible_for_resolved_time_range`] to pin +/// selection to the grid's index — and to keep raw queries off it. +#[cfg(any(feature = "server", feature = "verify"))] +pub fn resolve_time_range_bucket_clause( + field: &str, + selector: TimeRangeSelector, + grid: Option, + document_type: DocumentTypeRef, + block_time_ms: u64, +) -> Result<(WhereClause, ResolvedTimeRange), Error> { + // Distinct grids bucketing `field` — several indexes may share one grid + // (they share the storage level too), so dedupe by transform. + let mut grids: Vec<&TimeRangeTransform> = Vec::new(); + for index in document_type.indexes().values() { + if let Some(transform) = index + .time_range + .as_ref() + .filter(|transform| transform.source == field) + { + if !grids.contains(&transform) { + grids.push(transform); + } + } + } + if grids.is_empty() { + return Err(Error::Query( + QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "no time-range index is defined on field \"{}\"", + field + )), + )); + } + + let transform = match grid { + Some(spec) => *grids + .iter() + .find(|transform| spec.matches(transform)) + .ok_or(Error::Query(QuerySyntaxError::Unsupported(format!( + "no time-range index on \"{}\" declares the grid range={}s step={}s phase={}s", + field, spec.range_seconds, spec.step_seconds, spec.phase_seconds + ))))?, + None => { + if grids.len() > 1 { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "field \"{}\" is bucketed by {} different grids; the IN_TIME_RANGE operand \ + must name one as [selector, range, step] or [selector, range, step, phase] \ + (seconds, as the contract declares them)", + field, + grids.len() + )))); + } + grids[0] + } + }; + + let bucket_start = match selector { + TimeRangeSelector::Newest => transform.newest_active_start(block_time_ms), + TimeRangeSelector::Oldest => transform.oldest_active_start(block_time_ms), + } + .ok_or(Error::Query(QuerySyntaxError::Unsupported(format!( + "no time range on \"{}\" is active yet: the block time predates the grid's phase \ + anchor (only possible within the first step after the epoch)", + field + ))))?; + + Ok(( + WhereClause { + field: field.to_string(), + operator: WhereOperator::Equal, + value: Value::U64(bucket_start), + }, + ResolvedTimeRange { + transform: transform.clone(), + }, + )) +} + +/// Whether `index` may serve a query whose equality clauses on +/// `resolved_time_ranges` were produced by +/// [`resolve_time_range_bucket_clause`]. +/// +/// A time-range index does not store the source field's raw values: under its +/// grid-qualified first level it stores bucket *starts*, and one document is +/// stored once per bucket that contains its timestamp. So a bucketed index, a +/// raw index and another grid's bucketed index are never interchangeable, and +/// every mismatch is silent — a validly-proven wrong answer rather than an +/// error: +/// +/// - A raw query (`resolved_time_ranges` empty) that landed on a bucketed +/// index would compare a real timestamp against bucket starts and see +/// nothing (or, for range/IN shapes, walk overlapping buckets and count the +/// same document up to `overlap_factor` times). +/// - A resolved query that landed on a raw index would compare a bucket start +/// against real timestamps and see nothing. +/// - A resolved query that landed on a *different grid's* index would compare +/// one grid's bucket start against another grid's — every 6-hour start is +/// also a 3-hour start, so this can silently return the wrong window. +/// +/// Hence the rule: with no resolution only non-bucketed indexes are +/// admissible, and with one resolution only an index bucketing exactly that +/// field *with exactly that grid* is. Two resolutions can never be served by +/// a single index — a transform's source must be its index's first property, +/// so one index buckets exactly one field — and are rejected by the caller. +#[cfg(any(feature = "server", feature = "verify"))] +pub fn index_admissible_for_resolved_time_range( + index: &Index, + resolved_time_ranges: &[ResolvedTimeRange], +) -> bool { + match resolved_time_ranges { + [] => index.time_range.is_none(), + // The provenance's transform must equal the candidate's — grid AND + // source field, since the transform carries its own source. The + // provenance cannot name a field its grid does not bucket + // ([`ResolvedTimeRange::field`] is derived from the transform), so a + // fabricated field/transform pair is unrepresentable rather than + // guarded against. + [resolved] => index + .time_range + .as_ref() + .is_some_and(|transform| *transform == resolved.transform), + _ => false, + } +} + +/// Rejects a query whose resolution provenance and clause shapes disagree: +/// every field in `resolved_time_ranges` must appear in the where +/// clauses as exactly one `Equal` clause — the only shape +/// [`resolve_time_range_bucket_clause`] produces. +/// +/// A range or `In` clause on a resolved field means the caller attached +/// provenance to a clause the resolver never built. Executors that fan a +/// clause out per value (the per-`In`-value count/sum paths rewrite each `In` +/// value into an equality) would then present raw client values to the index +/// pickers as if they were resolved bucket starts, and the pickers would +/// admit the bucketed index for them. The wire path can never produce the +/// mismatch — provenance is not parseable from the wire, and the abci handler +/// pushes the resolved equality itself — so this guards direct API callers, +/// and it runs identically under `server` and `verify`. +#[cfg(any(feature = "server", feature = "verify"))] +pub fn validate_resolved_time_range_clause_shapes( + where_clauses: &[WhereClause], + resolved_time_ranges: &[ResolvedTimeRange], +) -> Result<(), Error> { + for field in resolved_time_ranges.iter().map(|resolved| resolved.field()) { + let mut equalities = 0usize; + for clause in where_clauses.iter().filter(|c| c.field == field) { + if clause.operator == WhereOperator::Equal { + equalities += 1; + } else { + return Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "a time-range-resolved field may only carry the single equality its \ + resolution produced, not a range or In clause", + ), + )); + } + } + if equalities != 1 { + return Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "a time-range-resolved field must carry exactly one equality clause — the \ + one its resolution produced", + ), + )); + } + } + Ok(()) +} + #[cfg(any(feature = "server", feature = "verify"))] /// Drive query struct #[derive(Debug, PartialEq, Clone)] @@ -592,6 +883,22 @@ pub struct DriveDocumentQuery<'a> { pub start_at_included: bool, /// Block time pub block_time_ms: Option, + /// The fields whose equality clause in `internal_clauses` was produced by + /// `IN_TIME_RANGE` resolution — i.e. by + /// [`resolve_time_range_bucket_clause`], on the server from committed + /// block time and in the verifier from the quorum-signed response metadata + /// time. + /// + /// Never parsed from the wire: every `from_cbor` / `from_value` / + /// `from_typed_clauses` entry point leaves this empty, so a client cannot + /// claim resolution it did not go through. It is what + /// [`Self::find_best_index`] uses to pin index selection to the index that + /// buckets the field (see [`index_admissible_for_resolved_time_range`]), + /// which is required because the resolved clause is an ordinary equality + /// and cannot be told apart from a raw-timestamp lookup once built. + /// + /// Empty for every raw query. + pub resolved_time_ranges: Vec, } impl<'a> DriveDocumentQuery<'a> { @@ -622,6 +929,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], } } @@ -638,6 +946,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at: None, start_at_included: true, block_time_ms: None, + resolved_time_ranges: vec![], } } @@ -658,6 +967,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at: None, start_at_included: true, block_time_ms: None, + resolved_time_ranges: vec![], } } @@ -884,6 +1194,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms, + resolved_time_ranges: vec![], }) } @@ -1030,6 +1341,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms, + resolved_time_ranges: vec![], }) } @@ -1194,6 +1506,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms: None, + resolved_time_ranges: vec![], }) } @@ -1611,10 +1924,37 @@ impl<'a> DriveDocumentQuery<'a> { /// ([`Self::find_best_index_for_multiple_in_clauses`]); they only /// reach it through the v1 (protocol version 14+) path-query /// lowering, since the v0 lowering rejects them first. + /// + /// Selection is restricted to the indexes admissible for this query's + /// [`Self::resolved_time_ranges`]: a query carrying an + /// `IN_TIME_RANGE`-resolved equality may only be served by the index that + /// buckets that field, and a raw query may never be served by a bucketed + /// index. See [`index_admissible_for_resolved_time_range`] for why either + /// mismatch would produce a validly-proven wrong answer. The rule applies + /// on both routes, including the multiple-`In` selection. pub fn find_best_index(&self, platform_version: &PlatformVersion) -> Result<&Index, Error> { + // A transform's source must be its index's first property, so one + // index buckets exactly one field and no index can carry two resolved + // equalities. Serving such a query would need a join across two + // bucketed indexes, which the engine has no shape for. This runs + // before any routing so the multiple-`In` path cannot bypass it. + if self.resolved_time_ranges.len() > 1 { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "at most one time-range selection (IN_TIME_RANGE) is supported per query; this \ + one resolves {:?}, and no single index can bucket more than one field", + self.resolved_time_ranges + )))); + } + + // One shared source-shape guard for every selection route — see its + // doc for the contract. Running it before routing keeps the single- + // and multiple-`In` routes rejecting the same shapes. + self.validate_resolved_source_shape()?; + if self.internal_clauses.in_clauses.len() > 1 { return Ok(self.find_best_index_for_multiple_in_clauses()?.0); } + let equal_fields = self .internal_clauses .equal_clauses @@ -1654,26 +1994,115 @@ impl<'a> DriveDocumentQuery<'a> { let (index, difference) = self .document_type - .index_for_types( + .index_for_types_matching( fields.as_slice(), in_field, order_by_keys.as_slice(), + |index| index_admissible_for_resolved_time_range(index, &self.resolved_time_ranges), platform_version, )? - .ok_or(Error::Query( - QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( - "query must be for valid indexes, valid indexes are: {:?}", - self.document_type.indexes() - )), - ))?; + .ok_or_else(|| match self.resolved_time_ranges.first() { + // A time-range query is only servable by the index that + // buckets the field with the resolved grid, so "no index" + // here is a narrower fact than the generic case: some index + // buckets the field (the clause could not have been resolved + // otherwise), but none with that grid also covers the rest + // of the query. + Some(resolved) => { + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "a time-range query on \"{}\" requires an index that buckets it with \ + the resolved grid AND covers the query's other where and order-by \ + fields; valid indexes are: {:?}", + resolved.field(), + self.document_type.indexes() + ))) + } + None => { + // A raw query never binds to a bucketed index; when one + // exists, say so — the caller may be holding a + // time-range proof on a surface that cannot supply + // resolution provenance (e.g. the standalone wasm + // verifiers), where this refusal is otherwise opaque. + let has_bucketed_index = self + .document_type + .indexes() + .values() + .any(|index| index.time_range.is_some()); + if has_bucketed_index { + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "query must be for valid indexes, valid indexes are: {:?}; note: \ + this document type's time-range (timeRange) indexes only serve \ + IN_TIME_RANGE selections carrying their resolution — a raw clause \ + on the bucketed field never binds to them", + self.document_type.indexes() + ))) + } else { + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "query must be for valid indexes, valid indexes are: {:?}", + self.document_type.indexes() + ))) + } + } + })?; if difference > defaults::MAX_INDEX_DIFFERENCE { return Err(Error::Query(QuerySyntaxError::QueryTooFarFromIndex( "query must better match an existing index", ))); } + + // The residual source-shape contract already ran at the top of this + // function ([`Self::validate_resolved_source_shape`]) — with a + // resolution present, admissibility restricts candidates to the one + // index bucketing exactly the resolved field, so guarding by + // provenance there is equivalent to guarding by the selected index's + // transform here. Ok(index) } + /// The residual source-shape contract for a query carrying a + /// time-range resolution: the resolved equality must be present on the + /// bucketed source, and the source must not ALSO carry an `In`, a + /// range, or an ordering — those walk overlapping bucket keys and + /// return each document up to `overlap_factor` times with a perfectly + /// valid proof. The `!has_equality_on_source` arm is defensive: + /// resolution always pushes the equality, so reaching it means the + /// provenance and the clauses disagree. + /// + /// Runs identically on the server and in proof verification, and on + /// every selection route: [`Self::find_best_index`] calls it before + /// routing, and [`Self::find_best_index_for_multiple_in_clauses`] + /// calls it itself because the multiple-`In` execution lowering picks + /// its index directly, without going through `find_best_index`. + #[cfg(any(feature = "server", feature = "verify"))] + pub(crate) fn validate_resolved_source_shape(&self) -> Result<(), Error> { + let Some(source) = self + .resolved_time_ranges + .first() + .map(|resolved| resolved.field()) + else { + return Ok(()); + }; + let has_equality_on_source = self.internal_clauses.equal_clauses.contains_key(source); + let range_or_in_on_source = self + .internal_clauses + .range_clause + .as_ref() + .is_some_and(|clause| clause.field == source) + || self + .internal_clauses + .in_clauses + .iter() + .any(|clause| clause.field == source); + if !has_equality_on_source || range_or_in_on_source || self.order_by.contains_key(source) { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "the index on \"{source}\" buckets it into time ranges: it can only be queried \ + through a time-range selection (IN_TIME_RANGE, which resolves to an exact \ + bucket equality), not with ranges, IN, or ordering on that property" + )))); + } + Ok(()) + } + #[cfg(any(feature = "server", feature = "verify"))] /// Returns a `QueryItem` given a start key and query direction. pub fn query_item_for_starts_at_key(starts_at_key: Vec, left_to_right: bool) -> QueryItem { @@ -2399,6 +2828,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let path_query = query_asc @@ -2779,6 +3209,55 @@ mod tests { .expect_err("starts with can not start with an empty string"); } + #[test] + fn resolved_time_range_shape_guard_accepts_only_the_single_resolution_equality() { + use crate::query::{validate_resolved_time_range_clause_shapes, ResolvedTimeRange}; + use dpp::data_contract::document_type::TimeRangeTransform; + + let resolved = vec![ResolvedTimeRange { + transform: TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 21_600, + step_seconds: 7_200, + phase_seconds: 0, + }, + }]; + let equality = WhereClause { + field: "$createdAt".to_string(), + operator: WhereOperator::Equal, + value: Value::U64(21_600_000), + }; + let other = WhereClause { + field: "hashtag".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("ibiza".to_string()), + }; + + validate_resolved_time_range_clause_shapes(&[equality.clone(), other.clone()], &resolved) + .expect("one equality on the resolved field is the resolution shape"); + + // An `In` on the resolved field would be fanned out per raw value by + // the aggregate executors and admitted against bucket keys. + let in_clause = WhereClause { + field: "$createdAt".to_string(), + operator: WhereOperator::In, + value: Value::Array(vec![Value::U64(0), Value::U64(7_200_000)]), + }; + validate_resolved_time_range_clause_shapes(&[in_clause, other.clone()], &resolved) + .expect_err("an In clause on a resolved field must be rejected"); + + let range_clause = WhereClause { + field: "$createdAt".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::U64(0), + }; + validate_resolved_time_range_clause_shapes(&[equality.clone(), range_clause], &resolved) + .expect_err("a range clause riding along on a resolved field must be rejected"); + + validate_resolved_time_range_clause_shapes(&[other], &resolved) + .expect_err("a resolved field with no equality at all must be rejected"); + } + #[test] fn test_withdrawal_query_with_missing_transaction_index() { // Setup the withdrawal contract @@ -2832,6 +3311,7 @@ mod tests { start_at: Some([3u8; 32]), start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // Create a document that we are starting at, which may be missing 'transactionIndex' @@ -2948,6 +3428,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], } } diff --git a/packages/rs-drive/src/query/non_primary_key_path_query/multiple_in_path_query/v0/mod.rs b/packages/rs-drive/src/query/non_primary_key_path_query/multiple_in_path_query/v0/mod.rs index 0cf2bb67207..11e13e9d1a2 100644 --- a/packages/rs-drive/src/query/non_primary_key_path_query/multiple_in_path_query/v0/mod.rs +++ b/packages/rs-drive/src/query/non_primary_key_path_query/multiple_in_path_query/v0/mod.rs @@ -11,7 +11,7 @@ use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::conditions::WhereClause; use crate::query::ordering::OrderClause; -use crate::query::{defaults, DriveDocumentQuery}; +use crate::query::{defaults, index_admissible_for_resolved_time_range, DriveDocumentQuery}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::{Index, IndexProperty}; @@ -230,14 +230,21 @@ impl<'a> DriveDocumentQuery<'a> { .map_err(Error::from)?; let mut path = document_type_path; + // Path segments are level keys: grid-qualified for a time-range + // index's first property (`Index::level_key_for_property`), the bare + // property name everywhere else. for (intermediate_index, intermediate_value) in index.properties[..equality_len] .iter() .zip(intermediate_values.iter()) { - path.push(intermediate_index.name.as_bytes().to_vec()); + path.push( + index + .level_key_for_property(&intermediate_index.name) + .into_bytes(), + ); path.push(intermediate_value.as_slice().to_vec()); } - path.push(child_field.as_bytes().to_vec()); + path.push(index.level_key_for_property(&child_field).into_bytes()); Ok(PathQuery::new( path, @@ -260,6 +267,13 @@ impl<'a> DriveDocumentQuery<'a> { pub(in crate::query) fn find_best_index_for_multiple_in_clauses( &self, ) -> Result<(&Index, Vec<&WhereClause>, usize), Error> { + // The execution lowering reaches this selection directly, without + // going through `find_best_index` — enforce the resolved-source + // shape contract here too, so a direct caller cannot ride an `In` + // or range on the bucketed source into per-value bucket keys. + // (`find_best_index` also runs this; the re-run is cheap.) + self.validate_resolved_source_shape()?; + let equal_clauses = &self.internal_clauses.equal_clauses; let in_clauses = &self.internal_clauses.in_clauses; let range_field = self @@ -292,6 +306,13 @@ impl<'a> DriveDocumentQuery<'a> { let equality_len = equal_clauses.len(); let mut best: Option<(&Index, Vec<&WhereClause>, u16)> = None; for index in self.document_type.indexes().values() { + // Same admissibility rule as the single-`In` selection in + // `find_best_index`: a bucketed index only for a query whose + // resolved equality names its transform source, never for a raw + // query. See `index_admissible_for_resolved_time_range`. + if !index_admissible_for_resolved_time_range(index, &self.resolved_time_ranges) { + continue; + } let mut positioned: Vec<(usize, &WhereClause)> = Vec::with_capacity(in_clauses.len()); for in_clause in in_clauses { match index diff --git a/packages/rs-drive/src/query/non_primary_key_path_query/single_in_path_query/v0/mod.rs b/packages/rs-drive/src/query/non_primary_key_path_query/single_in_path_query/v0/mod.rs index 891b87b7918..e7b4363da61 100644 --- a/packages/rs-drive/src/query/non_primary_key_path_query/single_in_path_query/v0/mod.rs +++ b/packages/rs-drive/src/query/non_primary_key_path_query/single_in_path_query/v0/mod.rs @@ -459,8 +459,24 @@ impl<'a> DriveDocumentQuery<'a> { // We should set the starts at document to be included for the query if there are // left over index properties. + // A time-range index's transformed first level stores bucket + // *starts*, so the cursor document's raw timestamp is not + // comparable to this level's keys: an included cursor created + // mid-bucket orders after the bucket-start key and would + // suppress it, validly proving an empty page. The resolved + // equality already pins this level to one key; the terminal + // document-id query attached below applies the cursor. + let last_clause_is_on_transformed_source = index + .time_range + .as_ref() + .is_some_and(|transform| transform.source == where_clause.field); + let query_starts_at_document = if left_over_index_properties.is_empty() { - &starts_at_document + if last_clause_is_on_transformed_source { + &None + } else { + &starts_at_document + } } else if sibling_aware_cursor_lowering { // The cursor's branch always stays included at this level, // trimming only the branches ordered before it; whether @@ -534,6 +550,72 @@ impl<'a> DriveDocumentQuery<'a> { &self.order_by, platform_version, )?; + } else if last_clause_is_on_transformed_source + && left_over_index_properties.is_empty() + { + // The cursor was deliberately withheld from the + // bucket-keyed level above; apply it here by + // bucket membership instead. A cursor inside the + // selected bucket continues the walk at its + // document id (for a unique index the bucket + // holds exactly the cursor document, so excluded + // means the page is exhausted); a cursor from + // outside the bucket cannot order within it and + // is ignored. + let cursor_in_bucket = match &starts_at_document { + None => None, + Some((document, included)) => { + let transform = index + .time_range + .as_ref() + .expect("checked by last_clause_is_on_transformed_source"); + let bucket_key = self.document_type.serialize_value_for_key( + where_clause.field.as_str(), + &where_clause.value, + platform_version, + )?; + document + .get_raw_for_document_type( + where_clause.field.as_str(), + self.document_type, + None, + platform_version, + )? + .filter(|raw| { + transform.entry_keys_for_raw(raw).contains(&bucket_key) + }) + .map(|_| (document, *included)) + } + }; + match cursor_in_bucket { + Some((document, included)) if !index.unique => { + query.set_subquery_key(vec![0]); + query.set_subquery(Self::inner_query_from_starts_at_for_id( + Some(&StartAtDocument { + document: document.clone(), + document_type: self.document_type, + included, + }), + left_to_right, + )); + } + cursor => { + if matches!(cursor, Some((_, false))) { + // Unique: the excluded cursor is the + // bucket's only document. + query = Query::new_with_direction(left_to_right); + } + Self::recursive_insert_on_query_ordered_with_cursor( + &mut query, + left_over_index_properties.as_slice(), + index.unique, + None, + left_to_right, + &self.order_by, + platform_version, + )?; + } + } } else { Self::recursive_insert_on_query_ordered_with_cursor( &mut query, @@ -602,14 +684,22 @@ impl<'a> DriveDocumentQuery<'a> { let mut path = document_type_path; + // Path segments are level keys: grid-qualified for a time-range + // index's first property (`Index::level_key_for_property`), the bare + // property name everywhere else. The values pushed between them are + // untouched — a bucket start is encoded exactly like a timestamp. for (intermediate_index, intermediate_value) in intermediate_indexes.iter().zip(intermediate_values.iter()) { - path.push(intermediate_index.name.as_bytes().to_vec()); + path.push( + index + .level_key_for_property(&intermediate_index.name) + .into_bytes(), + ); path.push(intermediate_value.as_slice().to_vec()); } - path.push(last_index.name.as_bytes().to_vec()); + path.push(index.level_key_for_property(&last_index.name).into_bytes()); Ok(PathQuery::new( path, diff --git a/packages/rs-drive/src/verify/document/verify_proof/mod.rs b/packages/rs-drive/src/verify/document/verify_proof/mod.rs index 46cde078b47..d7a383f4220 100644 --- a/packages/rs-drive/src/verify/document/verify_proof/mod.rs +++ b/packages/rs-drive/src/verify/document/verify_proof/mod.rs @@ -77,6 +77,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let result = query.verify_proof(&[], &platform_version); diff --git a/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/mod.rs b/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/mod.rs index 7909d0eb6bf..b19f32d2693 100644 --- a/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/mod.rs +++ b/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/mod.rs @@ -82,6 +82,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let result = query.verify_proof_keep_serialized(&[], &platform_version); diff --git a/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/mod.rs b/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/mod.rs index b2dc87e9a23..854902b78c1 100644 --- a/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/mod.rs +++ b/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/mod.rs @@ -94,6 +94,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; let result = diff --git a/packages/rs-drive/src/verify/document_having/mod.rs b/packages/rs-drive/src/verify/document_having/mod.rs index a3ddcec97e1..06d35824f66 100644 --- a/packages/rs-drive/src/verify/document_having/mod.rs +++ b/packages/rs-drive/src/verify/document_having/mod.rs @@ -10,9 +10,9 @@ //! //! Only one verifier exists here, for the same reason as on the ranked //! surface: every having-range request — any of the three axes, either -//! direction, any contiguous bound — resolves to one -//! `prove_indexed_axis_query` envelope that differs only in the Merk -//! query (the encoded bounds + direction) and limit it echoes. +//! direction, any contiguous bound — resolves to one bounded-axis +//! `PathQuery` proved through grovedb's unified `prove_query`, differing +//! only in the axis, bounds, direction and limit the query carries. /// Indexed-axis range proof verification — returns the groups the proof /// commits to as falling inside the bound, in axis order. diff --git a/packages/rs-drive/src/verify/document_having/verify_having_range_proof/mod.rs b/packages/rs-drive/src/verify/document_having/verify_having_range_proof/mod.rs index 893a2766bbb..9f202485af0 100644 --- a/packages/rs-drive/src/verify/document_having/verify_having_range_proof/mod.rs +++ b/packages/rs-drive/src/verify/document_having/verify_having_range_proof/mod.rs @@ -14,8 +14,8 @@ impl DriveDocumentHavingQuery<'_> { /// [`execute_range_with_proof`](Self::execute_range_with_proof). /// Both sides derive the proved subtree from /// [`indexed_property_name_tree_path`](Self::indexed_property_name_tree_path) - /// and the secondary query from - /// [`AxisRangeBounds::merk_query`](crate::query::drive_document_having_query::AxisRangeBounds::merk_query), + /// and the same bounded axis `PathQuery` from + /// [`AxisRangeBounds::i128_bounds`](crate::query::drive_document_having_query::AxisRangeBounds::i128_bounds), /// so the verifier cannot drift from the prover on *which* bound over /// *which* tree it is checking. /// diff --git a/packages/rs-drive/src/verify/document_having/verify_having_range_proof/v0/mod.rs b/packages/rs-drive/src/verify/document_having/verify_having_range_proof/v0/mod.rs index 9caf4f9df85..2cf0a0ebdf2 100644 --- a/packages/rs-drive/src/verify/document_having/verify_having_range_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/document_having/verify_having_range_proof/v0/mod.rs @@ -3,31 +3,32 @@ use crate::error::Error; use crate::query::{DriveDocumentHavingQuery, RankedAxis, RankedEntry, RankedEntryValue}; use crate::verify::RootHash; use dpp::version::PlatformVersion; -use grovedb::operations::proof::indexed_axis::AxisEntries; -use grovedb::GroveDb; +use grovedb::operations::proof::{indexed_axis::AxisEntries, VerifiedPathQuery}; +use grovedb::{GroveDb, PathQuery}; impl DriveDocumentHavingQuery<'_> { /// v0 of [`Self::verify_having_range_proof`]. /// - /// Rebuilds the proved subtree path with - /// [`Self::indexed_property_name_tree_path`] and the secondary query - /// with - /// [`AxisRangeBounds::merk_query`](crate::query::drive_document_having_query::AxisRangeBounds::merk_query), - /// then hands the proof to the matching - /// `GroveDb::verify_indexed_*_query` — an associated function, no - /// database handle, so this compiles and runs in a verifier-only - /// build. + /// Rebuilds the same [`PathQuery::new_axis_bounded`] the prover used + /// — the path via [`Self::indexed_property_name_tree_path`], the + /// bounds via + /// [`AxisRangeBounds::i128_bounds`](crate::query::drive_document_having_query::AxisRangeBounds::i128_bounds) + /// — then hands the proof to [`GroveDb::verify_path_query`] — an + /// associated function, no database handle, so this compiles and + /// runs in a verifier-only build. /// /// Three things are checked before the entries are returned: /// - /// 1. **The envelope matches this query.** grovedb re-checks the - /// proof against the reconstructed Merk query (the encoded bounds - /// and walk direction) and the expected limit, so a proof - /// generated for a different bound — or a different direction, or - /// a different limit — is rejected rather than silently - /// reinterpreted. Completeness rides on the same check: a Merk - /// range proof commits its boundaries, so an in-range group the - /// prover omitted fails reconstruction. + /// 1. **The proof answers this query.** The unified verifier is + /// query-as-input: nothing is echoed in the envelope; grovedb + /// lowers the query's bounds into the secondary's keyspace + /// through the same function the prover used and verifies the + /// proof against that reconstruction, so a proof generated for a + /// different bound — or a different direction, or a different + /// limit — is rejected rather than silently reinterpreted. + /// Completeness rides on the same check: a Merk range proof + /// commits its boundaries, so an in-range group the prover + /// omitted fails reconstruction. /// 2. **The result's axis shape matches the requested axis** — the /// same belt-and-braces check the ranked verifier does. /// 3. **At most `limit` entries.** Fewer is normal — fewer groups @@ -44,35 +45,35 @@ impl DriveDocumentHavingQuery<'_> { platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec), Error> { let path = self.indexed_property_name_tree_path()?; - let path_refs: Vec<&[u8]> = path.iter().map(|segment| segment.as_slice()).collect(); - let secondary_query = self.bounds.merk_query(self.descending); + let (lo, hi) = self.bounds.i128_bounds(); + let path_query = PathQuery::new_axis_bounded( + path, + self.bounds.axis().into(), + lo, + hi, + self.limit, + self.descending, + ); - let result = match self.bounds.axis() { - RankedAxis::Count => GroveDb::verify_indexed_count_query( - proof, - path_refs.as_slice(), - secondary_query, - Some(self.limit), - &platform_version.drive.grove_version, - ), - RankedAxis::Sum => GroveDb::verify_indexed_sum_query( - proof, - path_refs.as_slice(), - secondary_query, - Some(self.limit), - &platform_version.drive.grove_version, - ), - RankedAxis::Avg => GroveDb::verify_indexed_avg_query( - proof, - path_refs.as_slice(), - secondary_query, - Some(self.limit), - &platform_version.drive.grove_version, - ), - } - .map_err(|e| Error::GroveDB(Box::new(e)))?; + let verified = + GroveDb::verify_path_query(proof, &path_query, &platform_version.drive.grove_version) + .map_err(|e| Error::GroveDB(Box::new(e)))?; + + // `skipped` is the paginated traversal's field and is `None` for + // bounded ones — nothing to check here. + let VerifiedPathQuery::AxisEntries { + root_hash, + entries, + skipped: _, + } = verified + else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "having range proof for the {:?} axis verified to a non-axis result shape", + self.bounds.axis() + )))); + }; - let entries = match (self.bounds.axis(), result.entries) { + let entries = match (self.bounds.axis(), entries) { (RankedAxis::Count, AxisEntries::Count(entries)) => entries .into_iter() .map(|entry| entry.key_pair()) @@ -115,6 +116,6 @@ impl DriveDocumentHavingQuery<'_> { )))); } - Ok((result.root_hash, entries)) + Ok((root_hash, entries)) } } diff --git a/packages/rs-drive/src/verify/document_ranked/mod.rs b/packages/rs-drive/src/verify/document_ranked/mod.rs index 0a5a0e0b8a7..d99a6b887f1 100644 --- a/packages/rs-drive/src/verify/document_ranked/mod.rs +++ b/packages/rs-drive/src/verify/document_ranked/mod.rs @@ -12,8 +12,9 @@ //! 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. +//! resolves to one top-k axis `PathQuery` proved through grovedb's +//! unified `prove_query`, differing only in the `(axis, k, descending, +//! offset)` tuple the query carries. /// Indexed-axis top-k proof verification — returns the ranked groups the /// proof commits to, in ranking order. 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 index 82f5b0a2320..6e540052520 100644 --- 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 @@ -5,31 +5,33 @@ use crate::query::{ }; use crate::verify::RootHash; use dpp::version::PlatformVersion; -use grovedb::operations::proof::indexed_axis::AxisEntries; -use grovedb::GroveDb; +use grovedb::operations::proof::{indexed_axis::AxisEntries, VerifiedPathQuery}; +use grovedb::{GroveDb, PathQuery}; 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. + /// Rebuilds the same [`PathQuery::new_axis_top_k`] the prover used — + /// path via [`Self::indexed_property_name_tree_path`], the axis / + /// `k` / offset / direction from this query — and hands the proof to + /// [`GroveDb::verify_path_query`], 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. + /// 1. **The proof answers this query.** The unified verifier is + /// query-as-input: nothing is echoed in the envelope; the proof is + /// verified against the verifier's own reconstruction of the + /// `PathQuery`, so a proof generated for a different ranking — or + /// a different page of the same ranking — fails verification + /// rather than being 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. + /// is belt-and-braces on top of (1) (the query's axis already + /// rules it out) and exists so a future decoder change that + /// decouples the query 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. @@ -52,20 +54,41 @@ impl DriveDocumentRankedQuery<'_> { platform_version: &PlatformVersion, ) -> 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(), + let path_query = PathQuery::new_axis_top_k( + path, self.axis.into(), self.k, self.offset as u64, self.descending, - &platform_version.drive.grove_version, - ) - .map_err(|e| Error::GroveDB(Box::new(e)))?; + ); + + let verified = + GroveDb::verify_path_query(proof, &path_query, &platform_version.drive.grove_version) + .map_err(|e| Error::GroveDB(Box::new(e)))?; + + let VerifiedPathQuery::AxisEntries { + root_hash, + entries, + skipped, + } = verified + else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked top-k proof for the {:?} axis verified to a non-axis result shape", + self.axis + )))); + }; + + // A paginated (top-k) traversal always attests its skip count; + // `None` is the bounded traversal's shape and cannot answer this + // query. + let skipped = skipped.ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState(format!( + "ranked top-k proof for the {:?} axis carried no attested skip count", + self.axis + ))) + })?; - let entries = match (self.axis, result.entries) { + let entries = match (self.axis, entries) { (RankedAxis::Count, AxisEntries::Count(entries)) => entries .into_iter() .map(|entry| entry.key_pair()) @@ -108,12 +131,6 @@ impl DriveDocumentRankedQuery<'_> { )))); } - Ok(( - result.root_hash, - RankedPage { - skipped: result.skipped, - entries, - }, - )) + Ok((root_hash, RankedPage { skipped, entries })) } } diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index 67ee17dff46..31c72050120 100644 --- a/packages/rs-drive/tests/query_tests.rs +++ b/packages/rs-drive/tests/query_tests.rs @@ -9055,6 +9055,7 @@ mod withdrawal_in_clause_placement_equivalence { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; // The current shape: the In clause in in_clauses @@ -9076,6 +9077,7 @@ mod withdrawal_in_clause_placement_equivalence { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_ranges: vec![], }; for protocol_version in [13u32, 14u32] { diff --git a/packages/rs-platform-version/Cargo.toml b/packages/rs-platform-version/Cargo.toml index 4ee353c872e..74faf5deaf6 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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e" } +grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45" } [features] mock-versions = [] 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 84af8082e48..58280be26b8 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 @@ -76,8 +76,8 @@ pub struct DriveVerifyDocumentSumMethodVersions { /// Versions for the indexed-axis prove-path verifiers: the ranked /// (top-k) verifier and the boolean-`HAVING` range verifier. Both are /// implemented on the respective drive query types and delegate to -/// grovedb's indexed-axis proof verification -/// (`verify_indexed_axis_top_k_paginated` / `verify_indexed_axis_query`). +/// grovedb's unified `verify_path_query` over the query's axis +/// `PathQuery` (`new_axis_top_k` / `new_axis_bounded`). #[derive(Clone, Debug, Default)] pub struct DriveVerifyDocumentRankedMethodVersions { pub verify_ranked_top_k_proof: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index 192028514d2..76579f3a0d7 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -515,6 +515,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_contract_group_size: 256, max_token_redemption_cycles: 128, max_shielded_transition_actions: 16, + max_time_range_overlap_factor: None, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 4e5a8881c9d..0854a6c6f3b 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -86,6 +86,20 @@ pub struct SystemLimits { // do this that much pub max_token_redemption_cycles: u32, pub max_shielded_transition_actions: u16, + /// Maximum overlap factor (`range / step`) a `timeRange` index transform + /// may declare, enforced at contract registration. + /// + /// The overlap factor is the number of buckets that contain any given + /// timestamp — i.e. the write amplification of the index: every document + /// insert, delete, and (on a bucket-set change) update fans out into that + /// many index entries. The bound of 24 covers the natural worst case, a + /// day-long window sliding hourly, without letting a contract buy a + /// 256-entry fan-out per document. + /// + /// `None` preserves the behavior of protocol versions that predate + /// time-range indexes (nothing to bound: the `timeRange` keyword does not + /// parse there). + pub max_time_range_overlap_factor: Option, } #[cfg(test)] diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index 9f6a9218ac1..352d3ee490c 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -42,4 +42,5 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_time_range_overlap_factor: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index 0aff7000acd..3b3f46d5e43 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -30,4 +30,5 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_time_range_overlap_factor: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index 92dda4cf3d6..cdf3248de17 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -32,4 +32,5 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_time_range_overlap_factor: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index 486a50c77b9..efc8d72578e 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -2,13 +2,18 @@ use crate::version::system_limits::SystemLimits; /// System limits for protocol version 14 and above. /// -/// Identical to [`super::v3::SYSTEM_LIMITS_V3`] except that the daily withdrawal limit becomes -/// relative: `daily_withdrawal_limit_percent` is set to 15, so Platform pools at most 15% of the -/// total credits it held a day ago into asset unlock transactions per 24 hours — never below one -/// maximal withdrawal and never above `max_daily_withdrawal_amount`, Core's 4000 Dash unlock -/// capacity per day — instead of the flat 2000 Dash that applied from v8 (matching Core v22's -/// `LimitAmountV22`). v13 is already live on networks with the flat limit, so the change gates -/// here. +/// Identical to [`super::v3::SYSTEM_LIMITS_V3`] except for two changes: +/// +/// * The daily withdrawal limit becomes relative: `daily_withdrawal_limit_percent` is set to 15, +/// so Platform pools at most 15% of the total credits it held a day ago into asset unlock +/// transactions per 24 hours — never below one maximal withdrawal and never above +/// `max_daily_withdrawal_amount`, Core's 4000 Dash unlock capacity per day — instead of the +/// flat 2000 Dash that applied from v8 (matching Core v22's `LimitAmountV22`). v13 is already +/// live on networks with the flat limit, so the change gates here. +/// * `max_time_range_overlap_factor` is set: a `timeRange` index transform may declare at most +/// 24 overlapping windows per timestamp (a day-long window sliding hourly). The rule cannot +/// exist before v14 because the `timeRange` keyword itself is only admitted by the v14 +/// document meta-schema. pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { estimated_contract_max_serialized_size: 16384, max_field_value_size: 5120, //5 KiB @@ -35,4 +40,5 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_time_range_overlap_factor: Some(24), }; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index c461baedfe1..4d526efcabb 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -30,7 +30,7 @@ use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 hosts four consensus changes: +/// v14 hosts five consensus changes: /// /// 1. **Contract-level ranked aggregates** (this branch): an index can /// declare that its groups are rankable by an aggregate, so a query like @@ -93,6 +93,24 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// checked only at block level, so any daily total is still minable across /// blocks; after V24 it enforces 4000 Dash per 576-block window, which the /// cap above never exceeds. +/// 5. **Time-range indexes**: an index can declare a `timeRange` transform +/// that buckets a required system timestamp (`$createdAt` / +/// `$updatedAt` / `$transferredAt`) into fixed-length, regularly-spaced, +/// optionally overlapping windows declared in seconds (`range` / `step`, +/// plus an optional `phase < step` alignment offset). Each grid gets its +/// own index subtree — the level is keyed by the property name qualified +/// with the grid — so several grids may bucket one timestamp side by +/// side. A document is stored once per containing bucket per grid (the +/// v2 insert/delete and v1 update walkers carry the fan-out; the +/// per-document write amplification is capped per index by +/// `SystemLimits::max_time_range_overlap_factor`), and the v1 +/// `getDocuments` handler resolves the new `IN_TIME_RANGE` operator — +/// bare `"newest"`/`"oldest"` on a single-grid field, or a structured +/// `[selector, range, step(, phase)]` operand naming one grid — into a +/// bucket-start equality from committed block time, making "newest +/// window" trending/leaderboard document and count/sum/avg queries +/// provable. `unique: true` is admitted only for non-overlapping windows +/// (`range == step`) sourced from the immutable `$createdAt`. /// /// The first two are orthogonal by construction: the ranked upgrade decides the /// *property-name* tree type, the demotion decides the *value* tree type @@ -101,16 +119,17 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// variant did — so ranked secondaries keep ranking correctly over /// shared-prefix shapes. /// -/// Until a contract uses the ranked grammar, the only v14 behavior changes -/// are the shared-prefix fix, the contested-index cross-check, the -/// index-reorder schema-compatibility fix and the relative daily withdrawal -/// limit; everything else matches v13: +/// Until a contract uses the ranked or time-range grammar, the only v14 +/// behavior changes are the shared-prefix fix, the contested-index +/// cross-check, the index-reorder schema-compatibility fix and the relative +/// daily withdrawal limit; 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. +/// (`rankedCountable` / `rankedSummable` / `rankedAverageable`), the +/// `refersTo` reference keyword and the `timeRange` index transform. v13 +/// keeps validating against meta-schema v2, where those keys are rejected +/// as unknown properties, so a pre-v14 contract cannot smuggle them in. /// It also bumps `validate_schema_compatibility` to 1, which strips the /// top-level `indices` key before diffing the old and new document type /// schemas: index immutability is enforced by `validate_update` v1's @@ -122,7 +141,8 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// 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. +/// shared-prefix fix; those same walker versions carry the time-range +/// bucket fan-out, so both features gate on one table entry. /// * `DRIVE_ABCI_QUERY_VERSIONS_V3` bumps /// `document_query_helpers.compute_aggregate_mode_and_check_limit` 0 → 2, /// opening two routes on the v1 document-query handler: the ranked path @@ -156,10 +176,13 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// formats 0–2 (all pre-v14 documents) deserialize exactly as before with /// an unstamped (pre-annotation) layout. /// -/// The wire surface is deliberately unchanged: `GetDocumentsRequestV1` +/// The wire surface changes only additively: `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. +/// variant, whose `skipped` field is likewise additive; and the v1 +/// where-clause operator enum gains `IN_TIME_RANGE = 11`, which pre-v14 +/// servers reject as an unknown operator rather than misread (the v0 wire +/// has no time-range operator at all). pub const PLATFORM_V14: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_14, drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot @@ -168,7 +191,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, // changed: prune bound for the total credits history - query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate + query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate; the v1 handler also resolves IN_TIME_RANGE from committed block time checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, }, dpp: DPPVersion { @@ -178,7 +201,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_V6, // changed: v3 document meta-schema hosts the ranked index keywords + contract_versions: CONTRACT_VERSIONS_V6, // changed: v3 document meta-schema hosts the ranked, refersTo, requiredSince and timeRange keywords document_versions: DOCUMENT_VERSIONS_V4, // changed: document serialization format 3 — the contract version stamp that enables `requiredSince` properties identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, @@ -189,7 +212,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { }, system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, // changed: DashPay v2 adds profile payment address fields (DIP-33) fee_version: FEE_VERSION2, - system_limits: SYSTEM_LIMITS_V4, // changed: daily withdrawal limit becomes 15% of the total credits a day ago + system_limits: SYSTEM_LIMITS_V4, // changed: daily withdrawal limit becomes 15% of the total credits a day ago + time-range overlap-factor cap (24) consensus: ConsensusVersions { tenderdash_consensus_version: 1, }, diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index 755f7bb110b..a46939a40ca 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -69,7 +69,7 @@ zeroize = "1" log = "0.4" # Shielded pool (optional, behind `shielded` feature) -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", optional = true } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", 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 a0a7a919204..126a0803f3e 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 @@ -114,6 +114,7 @@ impl DashPayView<'_, B> { operator: WhereOperator::Equal, value: platform_value!(identity_id), }], + time_range_clauses: vec![], group_by: vec![], having: vec![], // Load-bearing, not cosmetic: drive answers a bare diff --git a/packages/rs-platform-wallet/src/wallet/identity/network/dpns_marketplace.rs b/packages/rs-platform-wallet/src/wallet/identity/network/dpns_marketplace.rs index 309a30f8800..11df0c62951 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/dpns_marketplace.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/dpns_marketplace.rs @@ -520,6 +520,7 @@ fn domain_by_normalized_label_query( value: Value::Text(normalized_label), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], @@ -561,6 +562,7 @@ fn history_by_source_document_query( value: Value::Identifier(source_document_id.to_buffer()), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![OrderClause { @@ -662,6 +664,7 @@ impl IdentityWallet { data_contract: contract, document_type_name: DPNS_DOCUMENT_TYPE.to_string(), where_clauses, + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![OrderClause { @@ -766,6 +769,7 @@ impl IdentityWallet { operator: WhereOperator::Equal, value: Value::Identifier(identity_id.to_buffer()), }], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], @@ -3699,6 +3703,7 @@ mod tests { operator: WhereOperator::Equal, value: Value::Identifier(identity_id.to_buffer()), }], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], 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 9cc79a181db..e4c041d2724 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs @@ -746,6 +746,7 @@ fn single_profile_query( operator: WhereOperator::Equal, value: platform_value!(identity_id), }], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], @@ -779,6 +780,7 @@ fn contact_profiles_chunk_query( operator: WhereOperator::In, value: in_values, }], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![OrderClause { diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index ee1bfca14ae..95a53f942cf 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", rev = "753a11f14c9a4bc72bf2d5302751dd43d174621e", features = [ +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "6c882c3ee7d2c331f1feda2eb4223add9a6f0e45", features = [ "client", "sqlite", ], optional = true } 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 5e4d20947ad..841e66676e3 100644 --- a/packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs +++ b/packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs @@ -105,6 +105,7 @@ impl Sdk { data_contract: dashpay_contract.clone(), document_type_name: "contactRequest".to_string(), where_clauses: where_clauses.clone(), + time_range_clauses: vec![], group_by: vec![], having: vec![], // Load-bearing: a bare secondary-index equality with no diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 2df034fad35..0955ea24454 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -341,6 +341,7 @@ impl Sdk { value: Value::Text(normalized_label), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], @@ -400,6 +401,7 @@ impl Sdk { value: Value::Text(normalized_label), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], diff --git a/packages/rs-sdk/src/platform/dpns_usernames/queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/queries.rs index 499091c505a..87f36b6ad03 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/queries.rs @@ -55,6 +55,7 @@ impl Sdk { operator: WhereOperator::Equal, value: Value::Identifier(identity_id.to_buffer()), }], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], // Remove ordering by $createdAt as it might not be indexed @@ -142,6 +143,7 @@ impl Sdk { value: Value::Text(normalized_prefix), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![OrderClause { diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index 52d7119cf54..a9acc2639ed 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -492,7 +492,12 @@ impl Query for DriveDocumentQuery<'_> { .to_string(), )); } - let q: DocumentQuery = self.into(); + // Fallible: a drive query carrying time-range resolution provenance + // has no faithful `DocumentQuery` form (the resolved bucket equality + // would demote to a raw-timestamp predicate) and is refused — build + // a `DocumentQuery` with `with_time_range` / `with_time_range_grid` + // for time-range selections instead. + let q: DocumentQuery = self.try_into()?; Ok(q) } } diff --git a/packages/rs-sdk/tests/fetch/document.rs b/packages/rs-sdk/tests/fetch/document.rs index 3edb4501325..fdfe8ee8417 100644 --- a/packages/rs-sdk/tests/fetch/document.rs +++ b/packages/rs-sdk/tests/fetch/document.rs @@ -132,6 +132,7 @@ async fn document_list_drive_query() { start_at: None, start_at_included: true, block_time_ms: None, + resolved_time_ranges: vec![], }; let docs = Document::fetch_many(&sdk, query) diff --git a/packages/wasm-drive-verify/src/document/verify_proof.rs b/packages/wasm-drive-verify/src/document/verify_proof.rs index b3cbfbbf726..1cd189c9f3e 100644 --- a/packages/wasm-drive-verify/src/document/verify_proof.rs +++ b/packages/wasm-drive-verify/src/document/verify_proof.rs @@ -108,6 +108,13 @@ pub fn verify_document_proof( start_at: start_at_bytes, start_at_included, block_time_ms, + // KNOWN LIMITATION: this surface has no input for time-range + // (IN_TIME_RANGE) resolution provenance, so proofs the server + // produced for a time-range query cannot be verified here — with + // empty provenance every bucketed index is inadmissible and + // verification fails closed. Use the SDK's FromProof path (which + // resolves from the signed metadata time) for those proofs. + resolved_time_ranges: vec![], }; let (root_hash, documents) = query diff --git a/packages/wasm-drive-verify/src/document/verify_proof_keep_serialized.rs b/packages/wasm-drive-verify/src/document/verify_proof_keep_serialized.rs index 5ff7f822cd0..53e73ac22ab 100644 --- a/packages/wasm-drive-verify/src/document/verify_proof_keep_serialized.rs +++ b/packages/wasm-drive-verify/src/document/verify_proof_keep_serialized.rs @@ -99,6 +99,13 @@ pub fn verify_document_proof_keep_serialized( start_at: start_at_bytes, start_at_included, block_time_ms, + // KNOWN LIMITATION: this surface has no input for time-range + // (IN_TIME_RANGE) resolution provenance, so proofs the server + // produced for a time-range query cannot be verified here — with + // empty provenance every bucketed index is inadmissible and + // verification fails closed. Use the SDK's FromProof path (which + // resolves from the signed metadata time) for those proofs. + resolved_time_ranges: vec![], }; let (root_hash, serialized_docs) = query diff --git a/packages/wasm-drive-verify/src/document/verify_start_at_document_in_proof.rs b/packages/wasm-drive-verify/src/document/verify_start_at_document_in_proof.rs index 3141260706c..12559c18701 100644 --- a/packages/wasm-drive-verify/src/document/verify_start_at_document_in_proof.rs +++ b/packages/wasm-drive-verify/src/document/verify_start_at_document_in_proof.rs @@ -107,6 +107,13 @@ pub fn verify_start_at_document_in_proof( start_at: start_at_bytes, start_at_included, block_time_ms, + // KNOWN LIMITATION: this surface has no input for time-range + // (IN_TIME_RANGE) resolution provenance, so proofs the server + // produced for a time-range query cannot be verified here — with + // empty provenance every bucketed index is inadmissible and + // verification fails closed. Use the SDK's FromProof path (which + // resolves from the signed metadata time) for those proofs. + resolved_time_ranges: vec![], }; let (root_hash, document_option) = query diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index d1c7099b5b7..c2f0677b4f7 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -277,6 +277,7 @@ impl WasmSdk { operator: WhereOperator::Equal, value: Value::Identifier(identity_id.to_buffer()), }], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 55f4c06f92e..abc3b36fe82 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -11,7 +11,7 @@ use dash_sdk::platform::documents::document_history_query::DocumentHistoryQuery; use dash_sdk::platform::documents::document_query::DocumentQuery; use dash_sdk::platform::Fetch; use dash_sdk::platform::FetchMany; -use drive::query::{OrderClause, WhereClause, WhereOperator}; +use drive::query::{OrderClause, TimeRangeGridSpec, TimeRangeSelector, WhereClause, WhereOperator}; use drive_proof_verifier::types::DocumentHistory; use drive_proof_verifier::{DocumentSplitAverages, DocumentSplitCounts, DocumentSplitSums}; use js_sys::{BigInt, Map}; @@ -119,6 +119,30 @@ export interface DocumentsQuery { * @default [] */ groupBy?: string[]; + + /** + * Time-range bucket selections for "trending"-style queries. Each entry + * picks a single bucket of a timestamp field covered by a `timeRange` + * index. The server resolves the bucket from the current block time and + * the proof verifier re-derives it from the signed response metadata, so + * the result is provable. Requires protocol version 14+ (the first + * version whose contract grammar hosts `timeRange` indexes). + * + * - `selector: "oldest"` → the oldest still-active range (a near-full + * trailing window of ~`range`; best for "trending over the last window"). + * - `selector: "newest"` → the freshest started range (latest partial slice). + * + * `grid` names one of the field's grids in the contract's own declared + * seconds (`{ range, step, phase? }`) — required when the contract buckets + * the field with more than one `timeRange` grid, where the bare selector + * is ambiguous and rejected. A zero phase is spelled by omission. + * @default [] + */ + timeRange?: { + field: string; + selector: "newest" | "oldest"; + grid?: { range: number; step: number; phase?: number }; + }[]; } /** @@ -195,6 +219,10 @@ struct DocumentsQueryInput { // `orderBy` field — the first clause's direction controls // split-mode entry ordering and `(In + prove)` walk order. No // separate `orderByAscending` knob. + /// Time-range bucket selections (`IN_TIME_RANGE`), each `{ field, + /// selector }`. v1-only; resolved server-side from block time. + #[serde(rename = "timeRange", default)] + time_range: Option>, } #[derive(Deserialize)] @@ -243,6 +271,7 @@ async fn build_documents_query( start_after, start_at, group_by: _, + time_range, } = input; let contract_id: Identifier = data_contract_id.into(); @@ -277,6 +306,16 @@ async fn build_documents_query( } } + if let Some(time_range_values) = time_range { + for clause_json in time_range_values.iter() { + let (field, selector, grid) = parse_time_range_clause(clause_json)?; + query = match grid { + Some(grid) => query.with_time_range_grid(field, selector, grid), + None => query.with_time_range(field, selector), + }; + } + } + if let Some(order_values) = order_by { for clause_json in order_values.iter() { let order_clause = parse_order_clause(clause_json)?; @@ -456,6 +495,75 @@ fn parse_where_clause(json_clause: &JsonValue) -> Result Result<(String, TimeRangeSelector, Option), WasmSdkError> { + let object = json_clause.as_object().ok_or_else(|| { + WasmSdkError::invalid_argument( + "timeRange clause must be an object { field, selector, grid? }", + ) + })?; + let field = object + .get("field") + .and_then(JsonValue::as_str) + .ok_or_else(|| { + WasmSdkError::invalid_argument("timeRange clause requires a string `field`") + })? + .to_string(); + let selector = object + .get("selector") + .and_then(JsonValue::as_str) + .and_then(TimeRangeSelector::from_string) + .ok_or_else(|| { + WasmSdkError::invalid_argument( + "timeRange clause `selector` must be \"newest\" or \"oldest\"", + ) + })?; + let grid = match object.get("grid") { + None | Some(JsonValue::Null) => None, + Some(grid_json) => { + let grid_object = grid_json.as_object().ok_or_else(|| { + WasmSdkError::invalid_argument( + "timeRange clause `grid` must be an object { range, step, phase? } in \ + the contract's declared seconds", + ) + })?; + let grid_number = |key: &str| -> Result { + grid_object + .get(key) + .and_then(JsonValue::as_u64) + .ok_or_else(|| { + WasmSdkError::invalid_argument(format!( + "timeRange clause `grid.{}` must be an unsigned integer of seconds, \ + exactly as the contract declares it", + key + )) + }) + }; + let range_seconds = grid_number("range")?; + let step_seconds = grid_number("step")?; + let phase_seconds = match grid_object.get("phase") { + None | Some(JsonValue::Null) => 0, + Some(_) => grid_number("phase")?, + }; + Some(TimeRangeGridSpec { + range_seconds, + step_seconds, + phase_seconds, + }) + } + }; + Ok((field, selector, grid)) +} + /// Parse JSON order by clause into OrderClause fn parse_order_clause(json_clause: &JsonValue) -> Result { let clause_array = json_clause @@ -1037,3 +1145,117 @@ fn split_averages_to_js_map(splits: Option) -> Result