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 c4e66dadcb9..2f341aafbba 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,19 @@ typedef GPB_ENUM(GetDocumentsRequest_WhereOperator) { GetDocumentsRequest_WhereOperator_BetweenExcludeRight = 8, GetDocumentsRequest_WhereOperator_In = 9, GetDocumentsRequest_WhereOperator_StartsWith = 10, + + /** + * Time-range bucket selection (v1 / `GetDocumentsRequestV1` only — the + * v0 CBOR where surface is unaffected). The clause's `field` names a + * timestamp property covered by a `timeRange` index; the operand + * (`DocumentFieldValue.text`) is the selector `"newest"` or `"oldest"`. + * The server resolves it to a concrete equality on the bucket start + * using the current block time, and the verifier re-derives the same + * bucket from the quorum-signed response metadata time — so the proof + * is 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 937962817c3..4d5aad2a98b 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -594,6 +594,16 @@ message GetDocumentsRequest { BETWEEN_EXCLUDE_RIGHT = 8; IN = 9; STARTS_WITH = 10; + // Time-range bucket selection (v1 / `GetDocumentsRequestV1` only — the + // v0 CBOR where surface is unaffected). The clause's `field` names a + // timestamp property covered by a `timeRange` index; the operand + // (`DocumentFieldValue.text`) is the selector `"newest"` or `"oldest"`. + // The server resolves it to a concrete equality on the bucket start + // using the current block time, and the verifier re-derives the same + // bucket from the quorum-signed response metadata time — so the proof + // is 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/rs-dpp/schema/meta_schemas/document/v3/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json index f5fb020a8b6..6cb5c51776f 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) and the refersTo reference keyword on identifier properties, 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, 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": { @@ -588,6 +588,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)." + }, + "origin": { + "type": "integer", + "minimum": 0, + "description": "Reference origin for range alignment, in seconds. Range starts are `origin + k * step`. 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`, `origin`) 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, and documents whose timestamp predates `origin` belong to no range (they are absent from this index). 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 c334f4b586f..93021e77fcd 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 @@ -22,6 +22,8 @@ use crate::data_contract::config::v0::DataContractConfigGettersV0; use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::class_methods::consensus_or_protocol_value_error; use crate::data_contract::document_type::index::Index; +#[cfg(feature = "validation")] +use crate::data_contract::document_type::index::TimeRangeTransform; use crate::data_contract::document_type::index_level::IndexLevel; use crate::data_contract::document_type::property::DocumentProperty; use crate::data_contract::document_type::property::DocumentPropertyType; @@ -192,6 +194,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 +496,7 @@ pub(super) fn parse_document_type_core( schema_map, &flags, &properties.flattened_document_properties, + &properties.required_fields, validation_operations, )?; @@ -777,6 +787,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 @@ -815,6 +826,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)?; @@ -845,6 +857,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, @@ -976,6 +1077,40 @@ fn parse_indices( // core never branches on a version. (ctx.generation.ranked_index_structure_check)(&indices)?; + // TIME RANGE: all indices that share a first property must agree on its + // time-range transform: either every such index buckets it with the + // identical transform, or none do. Otherwise the merged index trie node + // for that first property would be ambiguous (bucketed for one index, + // plain for another), so we reject the contract up front. No-op for + // generations whose grammar has no `timeRange`. + #[cfg(feature = "validation")] + if ctx.full_validation { + let mut first_property_time_range: BTreeMap<&str, Option<&TimeRangeTransform>> = + BTreeMap::new(); + for index in indices.values() { + let Some(first) = index.properties.first() else { + continue; + }; + let transform = index.time_range.as_ref(); + match first_property_time_range.get(first.name.as_str()) { + Some(existing) if *existing != transform => { + return Err(consensus_or_protocol_data_contract_error( + DataContractError::InvalidContractStructure(format!( + "indices that share the first property \"{}\" must agree on its \ + timeRange transform: either all bucket it identically or none \ + do", + first.name + )), + )); + } + Some(_) => {} + None => { + first_property_time_range.insert(first.name.as_str(), transform); + } + } + } + } + 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 db391c3456e..4cf9ce13ca8 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 @@ -422,6 +422,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, @@ -804,4 +831,147 @@ mod tests { ) .expect("a parse predating refersTo should ignore the keyword entirely"); } + + #[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..f322c35c812 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 @@ -100,6 +100,8 @@ impl DocumentTypeV1 { 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..f51e9e55087 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 @@ -259,6 +259,8 @@ fn try_from_schema_generation_3( admit_ranked: true, ranked_index_key_length_check: RANKED_INDEX_KEY_LENGTH_CHECK, ranked_index_structure_check: validate_no_ranked_prefix_overlap, + // TIME RANGE: the other keyword generation 3 adds. + admit_time_range: true, }, 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..719b73506d3 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,12 @@ 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"; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] @@ -476,6 +486,20 @@ 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, } impl Index { @@ -644,7 +668,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 +687,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 +750,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 +1006,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 origin_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(), + ) + })?); + } + "origin" => { + origin_seconds = tr_value.to_integer().map_err(|_| { + DataContractError::ValueWrongType( + "timeRange.origin 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, + origin_seconds, + }); + } "properties" => { let properties = value_value @@ -1221,6 +1337,156 @@ 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), + ("origin", transform.origin_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(), + )); + } + 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 +1531,7 @@ impl Index { ranked_countable, ranked_summable, ranked_averageable, + time_range, }) } } @@ -1335,9 +1602,292 @@ 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 + } + + #[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.origin_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 // ----------------------------------------------------------------------- @@ -2237,7 +2787,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 +2805,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 +2839,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 +2861,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 +2884,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 +2905,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 +2923,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 +2943,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 +2963,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 +2985,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 +3013,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 +3031,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 +3055,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 +3128,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 +3148,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 +3164,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 +3178,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 +3187,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 +3670,7 @@ mod json_convertible_tests { ranked_countable: true, ranked_summable: false, ranked_averageable: true, + time_range: None, } } @@ -3148,6 +3700,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..f9b190b6a4f --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/index/time_range.rs @@ -0,0 +1,392 @@ +#[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` / `origin` 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`. +/// 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 — e.g. per-hashtag counts within the bucket (`COUNT(*)` grouped by +/// `hashtag`, with the client ordering the returned groups). 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. +/// +/// This transform lives on the index definition only. At the GroveDB storage +/// layer 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` / `origin`, 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, + /// Reference origin for range alignment, in seconds. Range starts are the + /// millisecond timestamps `origin_ms() + k * step_ms()` for + /// `k = 0, 1, 2, …`. Defaults to `0`. + #[cfg_attr(feature = "serde-conversion", serde(rename = "origin", default))] + pub origin_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 alignment origin on the millisecond timeline — the first range's + /// start. See [`Self::range_ms`] for why the accessor exists and why it + /// saturates. + pub fn origin_ms(&self) -> u64 { + self.origin_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 start of the most recent range that has begun at or before the + /// millisecond timestamp `t`, i.e. the largest `origin + k * step` that is + /// `<= t`. + /// + /// Returns `None` for `t` before the origin: no range has started yet, and + /// the first range's window `[origin, origin + range)` does not contain + /// such a `t`, so there is no honest answer. (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, origin_ms) = (self.step_ms(), self.origin_ms()); + if step_ms == 0 || t < origin_ms { + return None; + } + let elapsed = t - origin_ms; + Some(origin_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 near the origin where + /// fewer ranges have started. For `t` before the origin the result is + /// empty: the timestamp predates every range, so the document is not + /// indexed under any bucket (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, origin_ms) = (self.step_ms(), self.origin_ms()); + (0..overlap) + .filter_map(|j| { + let offset = j.checked_mul(step_ms)?; + newest.checked_sub(offset) + }) + .filter(|start| *start >= origin_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` when `now` predates the origin: no range has started + /// yet, so there is no active bucket to query. + 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` when `now` predates the origin (no range has started + /// yet). + 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.origin_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 — and **no keys at all** when the timestamp predates the + /// origin (it belongs to no range; such a document is not present in + /// this index). + /// - A non-empty value that fails to decode 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()]; + } + match DocumentPropertyType::decode_date_timestamp(raw) { + 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, origin = 0 → overlap factor 3. + TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 6 * HOUR_SECONDS, + step_seconds: 2 * HOUR_SECONDS, + origin_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, + origin_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 origin is the first range + assert_eq!(t.most_recent_start(0), Some(0)); + } + + #[test] + fn pre_origin_timestamps_have_no_buckets() { + // A one-minute window stepping every twenty seconds, its grid anchored + // at the 1_000-second mark — 1_000_000 ms into the epoch. + let t = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + origin_seconds: 1_000, + }; + // a timestamp before the origin belongs to no range: it must not be + // indexed under any bucket, and no range is active yet + assert_eq!(t.most_recent_start(999_999), None); + assert_eq!(t.containing_buckets(999_999), Vec::::new()); + assert_eq!(t.newest_active_start(999_999), None); + assert_eq!(t.oldest_active_start(999_999), None); + // at the origin the first range starts + assert_eq!(t.most_recent_start(1_000_000), Some(1_000_000)); + assert_eq!(t.containing_buckets(1_000_000), vec![1_000_000]); + // every returned bucket actually contains the timestamp + for now in [1_000_000u64, 1_010_000, 1_059_000, 1_100_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_origin() { + 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), + ] + ); + // an undecodable non-empty value keeps its raw key + assert_eq!(t.entry_keys_for_raw(&[1, 2, 3]), vec![vec![1, 2, 3]]); + // a pre-origin timestamp belongs to no range: no keys + let t_offset = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + origin_seconds: 1_000, + }; + let raw = DocumentPropertyType::encode_date_timestamp(999_999); + assert_eq!(t_offset.entry_keys_for_raw(&raw), Vec::>::new()); + } + + #[test] + fn origin_offset_shifts_alignment() { + let t = TimeRangeTransform { + source: "$createdAt".to_string(), + range_seconds: 60, + step_seconds: 20, + origin_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]); + } +} 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..4939d1aad13 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, origin: {}s)", + t.source, t.range_seconds, t.step_seconds, t.origin_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..0baadf8966a 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,10 +7,12 @@ 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, }; use crate::data_contract::document_type::Index; +use crate::data_contract::errors::DataContractError; #[cfg(feature = "validation")] use crate::validation::SimpleConsensusValidationResult; use crate::version::PlatformVersion; @@ -121,6 +123,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 +144,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,17 +251,30 @@ impl IndexLevel { let mut index_level = IndexLevel { sub_index_levels: Default::default(), has_index_with_type: None, + time_range: None, level_identifier: 0, }; let mut counter: u64 = 0; + // First-property nodes that have already been visited, with the + // transform (or absence of one) their first visitor recorded. All + // indices sharing a first property must agree on its time-range + // transform — the walkers read the transform off the *merged* node, + // so a disagreement would bucket one index's entries and not + // another's. `try_from_schema` also rejects this under full + // validation; enforcing it here as well covers every construction + // path (check_tx, deserialized state, hand-built document types) + // instead of silently letting the last writer win. + let mut first_property_transforms: BTreeMap> = + BTreeMap::new(); + 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(); + let mut properties_iter = index.properties.iter().enumerate().peekable(); - while let Some(index_part) = properties_iter.next() { + while let Some((position, index_part)) = properties_iter.next() { current_level = current_level .sub_index_levels .entry(index_part.name.clone()) @@ -255,9 +284,37 @@ impl IndexLevel { level_identifier: counter, sub_index_levels: Default::default(), has_index_with_type: None, + time_range: None, } }); + // A time-range transform always targets the index's first + // property, so record it on that first-property node — + // rejecting any disagreement between indices that share it + // (see `first_property_transforms` above). + if position == 0 { + match first_property_transforms.get(&index_part.name) { + Some(existing) if *existing != index.time_range => { + return Err(ProtocolError::DataContractError( + DataContractError::InvalidContractStructure(format!( + "indices that share the first property \"{}\" must agree on \ + its timeRange transform: either all bucket it identically \ + or none do", + index_part.name + )), + )); + } + Some(_) => {} + None => { + first_property_transforms + .insert(index_part.name.clone(), index.time_range.clone()); + } + } + 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 +456,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 +501,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -464,6 +536,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![ @@ -483,6 +556,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, Index { name: "test2".to_string(), @@ -500,6 +574,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, ]; @@ -543,6 +618,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, Index { name: "test2".to_string(), @@ -560,6 +636,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }, ]; @@ -579,6 +656,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -620,6 +698,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -644,6 +723,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -691,6 +771,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -709,6 +790,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -750,6 +832,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -768,6 +851,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -809,6 +893,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -827,6 +912,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -868,6 +954,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -909,6 +996,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -927,6 +1015,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -968,6 +1057,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -986,6 +1076,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -1033,6 +1124,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -1057,6 +1149,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -1104,6 +1197,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let new_indices = vec![Index { @@ -1128,6 +1222,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }]; let old_index_structure = @@ -1183,6 +1278,7 @@ mod tests { ranked_countable, ranked_summable, ranked_averageable, + time_range: None, } } @@ -1339,6 +1435,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 88ce7c0f39d..62aad5faee8 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 588c826c383..f6680c1a148 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 @@ -400,10 +400,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..c52b08c235b 100644 --- a/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs @@ -112,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 - // validation — the fee must cover every index the - // contract actually registers. + // Same ranked-keyword / timeRange 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. 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. + let meta_schema_v3_grammar = platform_version + .dpp + .contract_versions + .document_type_versions + .schema + .document_type_schema + >= 3; if let Ok(index) = Index::try_from_value_map( index_value_map.as_slice(), - platform_version - .dpp - .contract_versions - .document_type_versions - .schema - .document_type_schema - >= 3, + meta_schema_v3_grammar, + meta_schema_v3_grammar, ) { 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/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..6ce8b55844d 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_range_fields: 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/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..c0a6beff292 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_range_fields: 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_range_fields: 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..5f66cc05563 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_range_fields: 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_range_fields: 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..10d0750bcb2 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_range_fields: 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..59b4cb4e641 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_range_fields: 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..93f2e129fdb 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_range_fields: 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_range_fields: 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_range_fields: 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_range_fields: 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..94848417b8f 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_range_fields: vec![], }; let documents = platform @@ -503,6 +504,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let documents = platform @@ -911,6 +913,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let documents = platform @@ -945,6 +948,7 @@ mod dpns_tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: 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_range_fields: vec![], }; platform @@ -1273,6 +1278,7 @@ mod dpns_username_transfer_tests { limit: None, prove: false, drive_config: &platform.config.drive, + resolved_time_range_fields: vec![], }; match platform @@ -1316,6 +1322,7 @@ mod dpns_username_transfer_tests { limit: None, prove: false, drive_config: &platform.config.drive, + resolved_time_range_fields: vec![], }; match platform @@ -1359,6 +1366,7 @@ mod dpns_username_transfer_tests { limit: None, prove: false, drive_config: &platform.config.drive, + resolved_time_range_fields: 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_range_fields: vec![], }; platform 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..b038b076eba 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_range_fields: 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_range_fields: 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 2bd36c1fad1..6065b1f49dd 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 @@ -125,6 +125,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 +160,7 @@ impl Platform { data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: Vec, order_by_clauses: Vec, limit_u32: Option, prove: bool, @@ -233,7 +237,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 +250,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_range_fields = resolved_time_range_fields; let response = if prove { let proof = @@ -644,6 +653,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let request = GetDocumentsRequestV0 { @@ -717,6 +727,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let request = GetDocumentsRequestV0 { @@ -802,6 +813,7 @@ mod tests { start_at: Some(after), start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let request = GetDocumentsRequestV0 { @@ -973,6 +985,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let where_clauses = serialize_vec_to_cbor( @@ -1139,6 +1152,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let where_clauses = serialize_vec_to_cbor( @@ -1293,6 +1307,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let mut where_clauses: Vec<_> = drive_document_query @@ -1458,6 +1473,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let mut where_clauses: Vec<_> = drive_document_query @@ -1640,6 +1656,7 @@ mod tests { start_at: Some(after.to_buffer()), start_at_included: false, block_time_ms: None, + resolved_time_range_fields: 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..59c3ce9632f 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 @@ -38,7 +38,7 @@ use dapi_grpc::platform::v0::get_documents_request::{ use dpp::platform_value::Value; 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 +49,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 +66,54 @@ 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)`. +/// The operand carries the selector as `DocumentFieldValue.text` +/// (`"newest"` or `"oldest"`). +pub(super) fn time_range_clause_from_proto( + clause: ProtoWhereClause, +) -> Result<(String, TimeRangeSelector), QueryError> { + let field = clause.field; + let selector_text = match clause.value.and_then(|v| v.variant) { + Some(document_field_value::Variant::Text(s)) => s, + _ => { + return Err(QueryError::InvalidArgument(format!( + "IN_TIME_RANGE clause on field '{}' must carry a text operand of \ + \"newest\" or \"oldest\"", + field + ))) + } + }; + let selector = TimeRangeSelector::from_string(&selector_text).ok_or_else(|| { + QueryError::InvalidArgument(format!( + "IN_TIME_RANGE selector must be \"newest\" or \"oldest\", got \"{}\"", + selector_text + )) + })?; + Ok((field, selector)) +} + /// 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..e92178868b6 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 @@ -46,6 +46,7 @@ impl Platform { data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: Vec, order_clauses: Vec, limit: Option, start: Option, @@ -102,6 +103,7 @@ impl Platform { document_type, sum_property, where_clauses, + resolved_time_range_fields, 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..4d07cd68bb3 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 @@ -42,6 +42,7 @@ impl Platform { data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: Vec, order_clauses: Vec, limit: Option, start: Option, @@ -88,6 +89,7 @@ impl Platform { contract: contract_ref, document_type, where_clauses, + resolved_time_range_fields, 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..1a3397bfb07 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 @@ -28,6 +28,7 @@ impl Platform { data_contract_id: Vec, document_type: String, where_clauses: Vec, + resolved_time_range_fields: Vec, order_by_clauses: Vec, limit: Option, start: Option, @@ -48,6 +49,7 @@ impl Platform { data_contract_id, document_type, where_clauses, + resolved_time_range_fields, order_by_clauses, limit, prove, 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..ed459465130 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 @@ -60,6 +60,7 @@ impl Platform { group_by: Vec, having: Vec, where_clauses: Vec, + resolved_time_range_fields: Vec, order_clauses: Vec, limit: Option, offset: Option, @@ -103,6 +104,7 @@ impl Platform { having: &having, order_by: &order_clauses, where_clauses: &where_clauses, + resolved_time_range_fields: &resolved_time_range_fields, 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..88dcafdef68 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 @@ -44,6 +44,7 @@ impl Platform { data_contract_id: Vec, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: Vec, order_clauses: Vec, limit: Option, start: Option, @@ -102,6 +103,7 @@ impl Platform { document_type, sum_property, where_clauses, + resolved_time_range_fields, 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..f09701edd18 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 @@ -55,9 +55,14 @@ 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::error::query::QuerySyntaxError; use drive::query::{CountMode, SelectProjection}; @@ -182,10 +187,94 @@ 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. + let (time_range_proto, normal_proto): (Vec<_>, Vec<_>) = proto_where_clauses + .into_iter() + .partition(conversions::is_time_range_clause); + + 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_range_fields: Vec = Vec::new(); + + 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: ContractIdentifier = + check_validation_result_with_data!(data_contract_id.clone().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 resolving a time range query", + )))); + 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) = match conversions::time_range_clause_from_proto(proto_wc) { + Ok(parsed) => parsed, + Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + }; + match drive::query::resolve_time_range_bucket_clause( + &field, + selector, + doc_type, + block_time_ms, + ) { + Ok(resolved) => { + where_clauses.push(resolved); + // The resolved clause is an ordinary equality; only + // this list tells the executors that it must be + // matched against bucket starts rather than raw + // timestamps, so it travels with the request. + resolved_time_range_fields.push(field); + } + Err(drive::error::Error::Query(qe)) => { + return Ok(QueryValidationResult::new_with_error(QueryError::Query(qe))) + } + Err(e) => return Err(e.into()), + } + } + } 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)), @@ -242,6 +331,7 @@ impl Platform { data_contract_id, document_type, where_clauses, + resolved_time_range_fields, order_by_clauses, limit, start, @@ -253,6 +343,7 @@ impl Platform { data_contract_id, document_type, where_clauses, + resolved_time_range_fields, order_by_clauses, limit, start, @@ -265,6 +356,7 @@ impl Platform { data_contract_id, document_type, where_clauses, + resolved_time_range_fields, order_by_clauses, limit, start, @@ -278,6 +370,7 @@ impl Platform { data_contract_id, document_type, where_clauses, + resolved_time_range_fields, order_by_clauses, limit, start, @@ -294,6 +387,7 @@ impl Platform { group_by, having_clauses, where_clauses, + resolved_time_range_fields, 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 3677b6bf0c3..0b4c0bd2a4f 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 @@ -366,7 +366,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 475419587a2..b0132076395 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 @@ -3852,8 +3852,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 { @@ -3891,7 +3897,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) @@ -4062,7 +4068,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, @@ -4237,3 +4243,570 @@ 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) + } + + /// 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 resolved = drive::query::resolve_time_range_bucket_clause( + CREATED_AT, + TimeRangeSelector::Newest, + *document_type, + time_ms, + ) + .expect("the metadata time falls inside an active range"); + let bucket_start = resolved + .value + .to_integer::() + .expect("a resolved bucket start is a millisecond timestamp"); + where_clauses.push(resolved); + let resolved_fields = vec![CREATED_AT.to_string()]; + + drive::query::validate_resolved_time_range_clause_shapes(&where_clauses, &resolved_fields) + .expect("resolution produces exactly the one equality the guard admits"); + + let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( + document_type.indexes(), + &where_clauses, + &resolved_fields, + ) + .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()), + }]; + where_clauses.push( + drive::query::resolve_time_range_bucket_clause( + CREATED_AT, + TimeRangeSelector::Newest, + document_type, + mtd.time_ms, + ) + .expect("the metadata time falls inside an active range"), + ); + 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_range_fields = vec![CREATED_AT.to_string()]; + + 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" + ); + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors_documents.rs b/packages/rs-drive-proof-verifier/tests/vectors_documents.rs index e6eb7753f3d..f0c74565eea 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_range_fields: vec![], } } diff --git a/packages/rs-drive/benches/document_count_worst_case.rs b/packages/rs-drive/benches/document_count_worst_case.rs index 2a00dd884c4..f3bb53c94ec 100644 --- a/packages/rs-drive/benches/document_count_worst_case.rs +++ b/packages/rs-drive/benches/document_count_worst_case.rs @@ -1800,6 +1800,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 { @@ -1817,6 +1818,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 { @@ -2282,6 +2284,7 @@ fn count_request<'a>( limit, prove, drive_config: &fixture.drive_config, + resolved_time_range_fields: vec![], } } diff --git a/packages/rs-drive/benches/document_sum_worst_case.rs b/packages/rs-drive/benches/document_sum_worst_case.rs index e1953d40b2e..56a138300c0 100644 --- a/packages/rs-drive/benches/document_sum_worst_case.rs +++ b/packages/rs-drive/benches/document_sum_worst_case.rs @@ -1601,6 +1601,7 @@ fn display_proofs(fixture: &SumBenchFixture, platform_version: &PlatformVersion) document_type, SUM_PROPERTY_NAME, &case.structured, + &[], platform_version, ) .expect("point-lookup path query builds"), @@ -1609,6 +1610,7 @@ fn display_proofs(fixture: &SumBenchFixture, platform_version: &PlatformVersion) document_type, SUM_PROPERTY_NAME, &case.structured, + &[], platform_version, ) .expect("aggregate-range path query builds"), @@ -1620,6 +1622,7 @@ fn display_proofs(fixture: &SumBenchFixture, platform_version: &PlatformVersion) document_type, SUM_PROPERTY_NAME, &case.structured, + &[], limit, left_to_right, platform_version, @@ -1913,6 +1916,7 @@ fn sum_request<'a>( limit, prove, drive_config: &fixture.drive_config, + resolved_time_range_fields: vec![], } } 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..ae2b4eea2f3 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_range_fields: &[], }, 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..cff9ee88c41 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_range_fields: 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 c4e30fd9927..10ff9287c29 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 @@ -795,6 +795,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 +1011,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] { @@ -1375,6 +1377,7 @@ fn ranked_avg_page( offset: Some(offset), has_start_at: false, prove: false, + resolved_time_range_fields: &[], }, None, platform_version(), @@ -1421,6 +1424,7 @@ fn verified_ranked_avg_page( offset: Some(offset), has_start_at: false, prove: true, + resolved_time_range_fields: &[], }, None, platform_version(), 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..200a3e86e6c 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; @@ -155,36 +159,58 @@ 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, - )?; + for index_key in index_keys { + 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.clone())) + } else { + PathInfo::PathAsVec::<0>(index_path.clone()) + }; + + // 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..028dd2d78d6 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,67 @@ 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, pre-origin 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) => { + let overlap = transform.overlap_factor().clamp(1, max_overlap_factor) 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 +392,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: true, + time_range: None, }; let compound = Index { name: "byRestaurantChef".to_string(), @@ -352,6 +416,7 @@ mod tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, }; let index_structure = 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..7948156be7f 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_range_fields: 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..24d182f6d51 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,14 @@ 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 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 +71,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 +353,71 @@ 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_range_fields = 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 cannot be a + // millisecond timestamp, so there is no bucket to + // probe and the index cannot be violated by it. + let timestamp = match &clause.value { + Value::U64(timestamp) => Some(*timestamp), + Value::I64(timestamp) => u64::try_from(*timestamp).ok(), + _ => None, + }; + let timestamp = timestamp?; + // A validated unique time-range index has overlap + // factor 1 (range == step), so a timestamp at or + // after the transform's origin yields exactly one + // containing bucket. An empty result means the + // timestamp predates the origin: 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_range_fields.push(transform.source.clone()); + } + let query = DriveDocumentQuery { contract, document_type, @@ -368,6 +434,7 @@ impl Drive { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields, }; // 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..74d6eb1616c 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,467 @@ 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::tests::utils::generate_random_identifier_struct; + 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; + /// Start of the window every "same window" timestamp below lands in, in + /// both units. It is a whole number of days, so it is a bucket start both + /// under the default origin (0) and under an origin of that same window. + const WINDOW_SECONDS: u64 = 100 * DAY_SECONDS; + const WINDOW_MS: u64 = 100 * DAY_MS; + + /// 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. + /// + /// `origin_seconds` shifts the window grid; `None` leaves it at the + /// default (0), where no `u64` timestamp can predate the origin. + fn build_unique_daily_report_contract(origin_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(origin_seconds) = origin_seconds { + time_range.push(( + Value::Text("origin".to_string()), + Value::U64(origin_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(generate_random_identifier_struct(), 0, schemas, None, None) + .expect("create contract") + .data_contract_owned() + } + + fn setup(platform_version: &'static PlatformVersion) -> (Drive, DataContract) { + setup_with_origin(None, platform_version) + } + + fn setup_with_origin( + origin_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(origin_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 = rand::random::<[u8; 32]>(); + let document = Document::V0(DocumentV0 { + id: Identifier::from(rand::random::<[u8; 32]>()), + 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` predating the transform's origin 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. + #[test] + fn pre_origin_candidate_skips_the_time_range_index_check() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_with_origin(Some(WINDOW_SECONDS), platform_version); + + // A stored report in the transform's first window. + insert_report( + &drive, + &contract, + WINDOW_MS + 3_600_000, + "alice", + platform_version, + ); + + // Same author, timestamp one millisecond before the origin: it belongs + // to no window, so there is nothing for it to duplicate. + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + WINDOW_MS - 1, + "alice", + UniquenessOfDataRequestUpdateType::NewDocument, + platform_version, + ); + assert!( + result.is_valid(), + "a pre-origin 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 origin talking and not a probe that silently + // stopped working on this contract. + let result = check_uniqueness( + &drive, + &contract, + Identifier::from([0xAA; 32]), + WINDOW_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..db1b8ae170d 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,1051 @@ impl Drive { } } } + +#[cfg(test)] +mod time_range_index_e2e_tests { + //! 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_range_fields`] rather than left to + //! whichever index happens to cover the fields. + use crate::config::DriveConfig; + use crate::drive::Drive; + use crate::query::DriveDocumentQuery; + 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::DataContractFactory; + use dpp::document::{Document, DocumentV0, DocumentV0Getters, DocumentV0Setters}; + use dpp::platform_value::{platform_value, Identifier, Value}; + use dpp::prelude::DataContract; + use dpp::tests::utils::generate_random_identifier_struct; + 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; + + /// 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 = generate_random_identifier_struct(); + 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, + vec!["$createdAt".to_string()], + ); + query + .execute_raw_results_no_proof(drive, None, None, platform_version) + .expect("query") + .0 + .len() + } + + /// A `$createdAt == created_at` query, optionally ANDed with + /// `hashtag == `, carrying `resolved_time_range_fields` verbatim + /// so tests can drive both the resolved and the raw (empty) provenance. + fn build_created_at_query<'a>( + contract: &'a DataContract, + document_type: dpp::data_contract::document_type::DocumentTypeRef<'a>, + created_at: u64, + hashtag: Option<&str>, + resolved_time_range_fields: 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_range_fields = resolved_time_range_fields; + 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 = rand::random::<[u8; 32]>(); + let document = Document::V0(DocumentV0 { + id: Identifier::from(rand::random::<[u8; 32]>()), + 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 = rand::random::<[u8; 32]>(); + let mut document = Document::V0(DocumentV0 { + id: Identifier::from(rand::random::<[u8; 32]>()), + 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`: + /// indexes sharing a first property must agree on its `timeRange` + /// transform, so a raw index can never lead with a bucketed field. + 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 = generate_random_identifier_struct(); + 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 = rand::random::<[u8; 32]>(); + let document = Document::V0(DocumentV0 { + id: Identifier::from(rand::random::<[u8; 32]>()), + 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"), + vec!["$createdAt".to_string()], + ); + 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 + ); + } + + /// 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_range_fields_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"), + vec!["$createdAt".to_string(), "hashtag".to_string()], + ); + let error = query + .find_best_index(PlatformVersion::latest()) + .expect_err("two resolved time-range fields cannot be served"); + assert!( + matches!( + error, + crate::error::Error::Query(crate::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_range_fields = vec!["$createdAt".to_string()]; + let error = query + .find_best_index(PlatformVersion::latest()) + .expect_err("no bucketed index covers the ordering"); + assert!( + matches!( + error, + crate::error::Error::Query( + crate::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 = generate_random_identifier_struct(); + 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_range_fields = vec!["$createdAt".to_string()]; + 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 = rand::random::<[u8; 32]>(); + let mut document = Document::V0(DocumentV0 { + id: Identifier::from(rand::random::<[u8; 32]>()), + 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 = rand::random::<[u8; 32]>(); + let second = Document::V0(DocumentV0 { + id: Identifier::from(rand::random::<[u8; 32]>()), + 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, + crate::error::Error::Query( + crate::error::query::QuerySyntaxError::WhereClauseOnNonIndexedProperty(_) + ) + ), + "expected a no-covering-index 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..125daccd081 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}; @@ -129,12 +133,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,16 +149,6 @@ 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 { @@ -192,40 +185,76 @@ 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), + ); + + for index_key in index_keys { + // 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.clone(), + value_tree_type, + storage_flags, + value_apply_type, + transaction, + previous_batch_operations, + batch_operations, + drive_version, + )?; - // 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 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.clone())) + } else { + PathInfo::PathAsVec::<0>(index_path.clone()) + }; + + // 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..ae1c0a43b12 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; @@ -357,6 +361,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 (source) node and `index_path` is the base + // (…//) at this point. The transform is + // read off the merged `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. (`IndexLevel::try_from_indices` rejects indices + // that share a first property but disagree on the transform, so + // the node's transform is every sharing index's transform.) + 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 +845,400 @@ 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 share the first property (and therefore, by the + // cross-index validation, the identical transform and buckets). + 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 8c878a8e407..14b9783102a 100644 --- a/packages/rs-drive/src/drive/document/update/mod.rs +++ b/packages/rs-drive/src/drive/document/update/mod.rs @@ -3262,6 +3262,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let sum_response = drive .execute_document_sum_request(sum_request, None, platform_version) @@ -3497,6 +3498,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: 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..ce3f5eee587 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_range_fields: 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_range_fields: 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..dbcd83938b5 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_range_fields: 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..d96e8fbb014 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_range_fields: vec![], }; // todo: deal with cost of this operation 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 526a5c86b57..5d62d51b984 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 @@ -76,6 +76,18 @@ impl Drive { 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. + crate::query::validate_resolved_time_range_clause_shapes( + &request.where_clauses, + &request.resolved_time_range_fields, + )?; if request.prove { return self.execute_document_average_prove(request, transaction, platform_version); } @@ -176,6 +188,7 @@ impl Drive { request.document_type.indexes(), &request.where_clauses, &request.sum_property, + &request.resolved_time_range_fields, ) .filter(|idx| idx.range_countable) .ok_or_else(|| { @@ -262,6 +275,7 @@ impl Drive { request.document_type.indexes(), &request.where_clauses, &request.sum_property, + &request.resolved_time_range_fields, ) .filter(|idx| idx.range_countable) .ok_or_else(|| { @@ -350,6 +364,7 @@ impl Drive { request.document_type.indexes(), &request.where_clauses, &request.sum_property, + &request.resolved_time_range_fields, ) .filter(|idx| idx.countable.is_countable()) .ok_or_else(|| { @@ -598,6 +613,7 @@ mod tests { limit: None, prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -615,6 +631,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`"); @@ -692,6 +709,7 @@ mod tests { limit: Some(over_max), prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let err = drive @@ -806,6 +824,7 @@ mod tests { limit: None, prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -883,6 +902,7 @@ mod tests { limit: None, prove: false, drive_config, + resolved_time_range_fields: vec![], }; let sum_request = DocumentSumRequest { contract, @@ -894,6 +914,7 @@ mod tests { limit: None, prove: false, drive_config, + resolved_time_range_fields: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1008,6 +1029,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let joint_response = drive @@ -1119,6 +1141,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let joint_response = drive @@ -1139,6 +1162,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let sum_request = DocumentSumRequest { contract: &data_contract, @@ -1150,6 +1174,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1253,6 +1278,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let joint_response = drive @@ -1273,6 +1299,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let sum_request = DocumentSumRequest { contract: &data_contract, @@ -1284,6 +1311,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1376,6 +1404,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let joint_response = drive @@ -1508,6 +1537,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let joint_response = drive @@ -1528,6 +1558,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let sum_request = DocumentSumRequest { contract: &data_contract, @@ -1539,6 +1570,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let count_resp = drive .execute_document_count_request(count_request, None, platform_version) @@ -1641,6 +1673,7 @@ mod tests { limit: Some(2), prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -1719,6 +1752,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -1797,6 +1831,7 @@ mod tests { limit: Some(4), prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -1852,6 +1887,7 @@ mod tests { limit: None, prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let err = drive @@ -1914,6 +1950,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let err = drive @@ -1932,6 +1969,60 @@ 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_range_fields: vec!["$createdAt".to_string()], + }; + + 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 @@ -2016,6 +2107,7 @@ mod tests { limit: Some(2), prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -2144,6 +2236,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -2250,6 +2343,7 @@ mod tests { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: 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..bd51842ee07 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 @@ -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_range_fields`]: + /// it is what gates which indexes the sum pickers (which average rides) + /// may select. + pub resolved_time_range_fields: 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..e9da98f98e2 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 @@ -111,6 +111,11 @@ impl Drive { // the catalog of rejections. let where_clauses = validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; + let resolved_time_range_fields = request.resolved_time_range_fields; + crate::query::validate_resolved_time_range_clause_shapes( + &where_clauses, + &resolved_time_range_fields, + )?; // Convert AverageMode → SumMode (1:1 by construction); sum's // routing table is the single source of truth for the @@ -145,6 +150,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, transaction, platform_version, @@ -170,6 +176,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, order_by_ascending, per_in_limit as u16, @@ -214,6 +221,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, 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..edf8f28d5d1 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 @@ -48,6 +48,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, left_to_right: bool, limit: u16, @@ -122,6 +123,7 @@ impl Drive { document_type.indexes(), &clauses_for_value, &sum_property, + resolved_time_range_fields, ) .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..de1ebf3b00d 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 @@ -71,6 +71,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, return_distinct: bool, left_to_right: bool, @@ -82,6 +83,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..723578efac4 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 @@ -54,6 +54,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -89,6 +90,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..1d696c3451a 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 @@ -62,6 +62,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_range_fields`]: + /// it is what gates which indexes the count pickers may select. + pub resolved_time_range_fields: 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 @@ -445,6 +451,11 @@ 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_range_fields = request.resolved_time_range_fields; + crate::query::validate_resolved_time_range_clause_shapes( + &where_clauses, + &resolved_time_range_fields, + )?; let order_clauses = request.order_clauses; // Split-mode entry direction is whatever the first orderBy @@ -476,6 +487,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, transaction, platform_version, )?; @@ -497,6 +509,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, options, transaction, platform_version, @@ -537,6 +550,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, options, transaction, platform_version, @@ -558,6 +572,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, transaction, platform_version, )?, @@ -617,6 +632,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, limit_u16, left_to_right, transaction, @@ -630,6 +646,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, transaction, platform_version, )?, @@ -717,6 +734,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, 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..00f071851df 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 @@ -37,6 +37,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], options: RangeCountOptions, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -97,6 +98,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &clauses_for_value, + resolved_time_range_fields, ) .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..9dc6c265e71 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 @@ -32,12 +32,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_range_fields: &[String], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -69,6 +71,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_range_fields, ) .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..0514a43034b 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 @@ -55,6 +55,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], limit: Option, left_to_right: bool, transaction: TransactionArg, @@ -63,6 +64,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_range_fields, ) .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..ec9dce8f109 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 @@ -36,6 +36,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], limit: u16, left_to_right: bool, transaction: TransactionArg, @@ -44,6 +45,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_range_fields, ) .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..0d5c1bf1d42 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 @@ -26,6 +26,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], options: RangeCountOptions, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -33,6 +34,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_range_fields, ) .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..d0d8decbb0f 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 @@ -18,18 +18,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_range_fields: &[String], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { let index = DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_range_fields, ) .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..4590324620b 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 @@ -18,12 +18,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_range_fields: &[String], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -56,6 +58,7 @@ impl Drive { let index = DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &where_clauses, + resolved_time_range_fields, ) .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..7180916f3f5 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,7 @@ use super::super::conditions::WhereClause; use super::DriveDocumentCountQuery; +use crate::query::index_admissible_for_resolved_time_range; use dpp::data_contract::document_type::Index; use std::collections::{BTreeMap, BTreeSet}; @@ -33,9 +34,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_range_fields` 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_range_fields: &[String], ) -> Option<&'b Index> { if Self::has_unsupported_operator(where_clauses) { return None; @@ -56,6 +64,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_range_fields) { + continue; + } if !index.countable.is_countable() { continue; } @@ -93,9 +109,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_range_fields` 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_range_fields: &[String], ) -> Option<&'b Index> { let range_clauses: Vec<&WhereClause> = where_clauses .iter() @@ -147,6 +172,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_range_fields) { + continue; + } if !index.range_countable || !index.countable.is_countable() { continue; } 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 da54e064fc9..a25ab46bce4 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 @@ -141,6 +141,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"); @@ -199,6 +200,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(), @@ -216,6 +218,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(), @@ -233,6 +236,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); @@ -266,6 +270,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!( @@ -341,6 +346,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() ); @@ -373,6 +379,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"); @@ -418,6 +425,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"); @@ -532,6 +540,7 @@ fn test_aggregate_count_in_fan_out_ignores_default_query_limit() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -602,6 +611,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"); @@ -690,6 +700,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 @@ -813,6 +824,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 @@ -942,6 +954,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 — @@ -1091,6 +1104,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); @@ -1329,6 +1343,7 @@ fn test_compound_range_in_summed_no_proof_uses_per_in_aggregate_fanout() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -1403,6 +1418,7 @@ fn test_count_request_with_duplicate_equality_clauses_is_rejected() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let err = drive @@ -1598,6 +1614,7 @@ fn test_range_distinct_proof_uses_compile_time_default_query_limit_not_operator_ limit: None, prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -1626,6 +1643,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 { @@ -1721,6 +1739,7 @@ fn test_range_distinct_no_proof_rejects_zero_effective_limit() { limit: None, prove: false, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let result = drive.execute_document_count_request(request, None, platform_version); @@ -1767,6 +1786,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), @@ -2003,6 +2023,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); @@ -2076,6 +2097,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"); @@ -2143,6 +2165,7 @@ mod range_countable_picker_tests { ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } @@ -2168,6 +2191,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"); @@ -2201,6 +2225,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"); @@ -2227,6 +2252,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 \ @@ -2254,6 +2280,7 @@ mod range_countable_picker_tests { DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( &indexes, &where_clauses, + &[], ) .is_none() ); @@ -2285,6 +2312,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)" @@ -2310,6 +2338,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" @@ -2895,6 +2924,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!( @@ -3011,6 +3041,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); @@ -3178,6 +3209,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); @@ -3297,6 +3329,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!( @@ -3370,3 +3403,176 @@ 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, + origin_seconds: 0, + }), + ), + ] + .into_iter() + .map(|index| (index.name.clone(), index)) + .collect() + } + + 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.to_string()], + ) + .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.to_string()], + ) + .is_none() + ); + } +} 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..5743d178a7a 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,6 +17,7 @@ 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; @@ -64,6 +65,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_range_fields`]). + /// 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_range_fields: &'a [String], /// Request `limit` — the ranking's `k`. **Required**; there is no /// server default a verifying client could reproduce. pub limit: Option, @@ -128,6 +139,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_range_fields.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/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/tests.rs b/packages/rs-drive/src/query/drive_document_ranked_query/tests.rs index 4a477c98195..0ae2fb3c4c9 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 @@ -689,6 +689,7 @@ fn test_index(name: &str, properties: &[&str], summable: Option<&str>) -> Index ranked_countable: false, ranked_summable: false, ranked_averageable: false, + time_range: None, } } @@ -960,6 +961,7 @@ fn run( offset: case.offset, has_start_at: false, prove, + resolved_time_range_fields: &[], }, None, platform_version(), @@ -2244,6 +2246,7 @@ mod pinned_prefix { offset: None, has_start_at: false, prove, + resolved_time_range_fields: &[], }, None, platform_version(), @@ -2461,6 +2464,7 @@ mod pinned_prefix { offset: None, has_start_at: false, prove, + resolved_time_range_fields: &[], }; 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..c7a8c483802 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 @@ -62,6 +62,14 @@ impl Drive { 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_range_fields = request.resolved_time_range_fields; + // Same provenance-vs-shape contract as the count and joint + // dispatchers; sum has no canonicalize step, so the guard anchors + // here. + crate::query::validate_resolved_time_range_clause_shapes( + &where_clauses, + &resolved_time_range_fields, + )?; 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 +86,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, transaction, platform_version, @@ -97,6 +106,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, options, transaction, @@ -127,6 +137,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, options, transaction, @@ -145,6 +156,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, transaction, platform_version, @@ -196,6 +208,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, limit_u16, order_by_ascending, @@ -210,6 +223,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, sum_property, transaction, platform_version, @@ -254,6 +268,7 @@ impl Drive { request.document_type, document_type_name, where_clauses, + &resolved_time_range_fields, 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..219320ece8e 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 @@ -29,6 +29,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, options: RangeSumOptions, transaction: TransactionArg, @@ -82,6 +83,7 @@ impl Drive { document_type.indexes(), &clauses_for_value, &sum_property, + resolved_time_range_fields, ) .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..8c8ea73514c 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 @@ -25,6 +25,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -59,6 +60,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..c50666d36e5 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 @@ -45,6 +45,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, limit: Option, left_to_right: bool, @@ -55,6 +56,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..786956442c7 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 @@ -26,6 +26,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, limit: u16, left_to_right: bool, @@ -36,6 +37,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..6c7038c5d22 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 @@ -23,6 +23,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, options: RangeSumOptions, transaction: TransactionArg, @@ -32,6 +33,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..d793e0fa96d 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 @@ -25,6 +25,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -33,6 +34,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..70e428aa818 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 @@ -30,6 +30,7 @@ impl Drive { document_type: DocumentTypeRef, document_type_name: String, where_clauses: Vec, + resolved_time_range_fields: &[String], sum_property: String, transaction: TransactionArg, platform_version: &PlatformVersion, @@ -65,6 +66,7 @@ impl Drive { document_type.indexes(), &where_clauses, &sum_property, + resolved_time_range_fields, ) .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..6d6f5a6a22d 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,7 @@ //! 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::{index_admissible_for_resolved_time_range, WhereClause, WhereOperator}; use dpp::data_contract::document_type::Index; use std::collections::{BTreeMap, BTreeSet}; @@ -26,10 +26,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_range_fields` 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_range_fields: &[String], ) -> Option<&'b Index> { // Defense-in-depth: any non-indexable operator immediately disqualifies // — the sum point-lookup path can only serve Equal/In. @@ -51,6 +57,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_range_fields) { + continue; + } // Skip if not summable OR if summable property doesn't match. match &index.summable { Some(prop) if prop == sum_property => {} @@ -77,10 +91,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_range_fields` 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_range_fields: &[String], ) -> Option<&'b Index> { let range_clauses: Vec<&WhereClause> = where_clauses .iter() @@ -121,6 +144,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_range_fields) { + 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..928307224ea 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 @@ -180,6 +180,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_range_fields`]: + /// it is what gates which indexes the sum pickers may select. + pub resolved_time_range_fields: 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..7ee7bf94a88 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 @@ -1107,6 +1107,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type: DocumentTypeRef, sum_property: &str, where_clauses: &[WhereClause], + resolved_time_range_fields: &[String], platform_version: &PlatformVersion, ) -> Result { use crate::query::drive_document_sum_query::index_picker::find_summable_index_for_where_clauses; @@ -1117,6 +1118,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type.indexes(), where_clauses, sum_property, + resolved_time_range_fields, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( @@ -1145,6 +1147,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type: DocumentTypeRef, sum_property: &str, where_clauses: &[WhereClause], + resolved_time_range_fields: &[String], platform_version: &PlatformVersion, ) -> Result { use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses; @@ -1155,6 +1158,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type.indexes(), where_clauses, sum_property, + resolved_time_range_fields, ) .ok_or_else(|| { Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty( @@ -1183,11 +1187,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_range_fields: &[String], limit: Option, left_to_right: bool, platform_version: &PlatformVersion, @@ -1200,6 +1206,7 @@ impl<'a> DriveDocumentSumQuery<'a> { document_type.indexes(), where_clauses, sum_property, + resolved_time_range_fields, ) .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 1688669039b..6b812a34f01 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() ); } @@ -451,6 +468,7 @@ mod limit_policy_regression { limit: None, prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let response = drive @@ -471,6 +489,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 { @@ -547,6 +566,7 @@ mod limit_policy_regression { limit: Some(over_max), prove: true, drive_config: &drive_config, + resolved_time_range_fields: vec![], }; let err = drive @@ -618,6 +638,7 @@ mod limit_policy_regression { limit, prove: false, drive_config: &drive_config, + resolved_time_range_fields: 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 a601d420761..4d66ada1487 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -570,6 +570,192 @@ 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, + } + } +} + +/// Resolves a time-range selection on `field` into a concrete equality +/// [`WhereClause`] on the bucketed source field, using the index'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. +/// +/// What comes back is an ordinary equality clause, byte-identical to one a +/// client could have written by hand against a raw timestamp. The fact that it +/// was *produced here* is what makes it safe to run against an index whose keys +/// are bucket starts, and that provenance is not recoverable from the clause: +/// callers must record `field` in the query's `resolved_time_range_fields` +/// (see [`DriveDocumentQuery::resolved_time_range_fields`]), which +/// [`DriveDocumentQuery::find_best_index`] and the aggregate index pickers +/// consume through [`index_admissible_for_resolved_time_range`] to pin +/// selection to the bucketed 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, + document_type: DocumentTypeRef, + block_time_ms: u64, +) -> Result { + // Every index bucketing `field` uses the same transform for it — the + // transform's source must be the index's first property, and two indexes + // bucketing the same field with different windows would be two different + // sources of truth for one clause — so the first match is the transform. + let transform = document_type + .indexes() + .values() + .find_map(|index| { + index + .time_range + .as_ref() + .filter(|transform| transform.source == field) + }) + .ok_or(Error::Query( + QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "no time-range index is defined on field \"{}\"", + field + )), + ))?; + + 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 current block time predates the index's \ + origin", + field + ))))?; + + Ok(WhereClause { + field: field.to_string(), + operator: WhereOperator::Equal, + value: Value::U64(bucket_start), + }) +} + +/// Whether `index` may serve a query whose equality clauses on +/// `resolved_time_range_fields` were produced by +/// [`resolve_time_range_bucket_clause`]. +/// +/// A time-range index does not store the source field's raw values: under its +/// first property it stores bucket *starts*, and one document is stored once +/// per bucket that contains its timestamp. So the two kinds of index are not +/// interchangeable in either direction, and both mismatches are silent — +/// they return a validly-proven wrong answer rather than an error: +/// +/// - A raw query (`resolved_time_range_fields` 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. +/// +/// Hence the rule: with no resolved field only non-bucketed indexes are +/// admissible, and with one resolved field only the index bucketing exactly +/// that field is. Two resolved fields 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_range_fields: &[String], +) -> bool { + match resolved_time_range_fields { + [] => index.time_range.is_none(), + [field] => index + .time_range + .as_ref() + .is_some_and(|transform| transform.source == *field), + _ => false, + } +} + +/// Rejects a query whose resolution provenance and clause shapes disagree: +/// every field in `resolved_time_range_fields` 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_range_fields: &[String], +) -> Result<(), Error> { + for field in resolved_time_range_fields { + 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 +778,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_range_fields: Vec, } impl<'a> DriveDocumentQuery<'a> { @@ -622,6 +824,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], } } @@ -638,6 +841,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at: None, start_at_included: true, block_time_ms: None, + resolved_time_range_fields: vec![], } } @@ -658,6 +862,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at: None, start_at_included: true, block_time_ms: None, + resolved_time_range_fields: vec![], } } @@ -884,6 +1089,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms, + resolved_time_range_fields: vec![], }) } @@ -1030,6 +1236,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms, + resolved_time_range_fields: vec![], }) } @@ -1194,6 +1401,7 @@ impl<'a> DriveDocumentQuery<'a> { start_at, start_at_included, block_time_ms: None, + resolved_time_range_fields: vec![], }) } @@ -1602,10 +1810,64 @@ 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_range_fields`]: 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_range_fields.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_range_fields + )))); + } + if self.internal_clauses.in_clauses.len() > 1 { + // The multiple-`In` selection filters its candidates through the + // same admissibility rule, but it returns early and so bypasses + // the residual source-shape guard at the bottom of this function. + // Enforce the resolved-field contract here instead: the resolved + // equality must be present, and the bucketed source must not also + // carry an `In`, a range, or an ordering. + if let Some(source) = self.resolved_time_range_fields.first() { + let has_equality_on_source = + self.internal_clauses.equal_clauses.contains_key(source); + let in_or_range_on_source = self + .internal_clauses + .in_clauses + .iter() + .any(|clause| &clause.field == source) + || self + .internal_clauses + .range_clause + .as_ref() + .is_some_and(|clause| &clause.field == source); + if !has_equality_on_source + || in_or_range_on_source + || self.order_by.contains_key(source) + { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "the index on \"{}\" 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", + source + )))); + } + } return Ok(self.find_best_index_for_multiple_in_clauses()?.0); } + let equal_fields = self .internal_clauses .equal_clauses @@ -1645,23 +1907,81 @@ 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_range_fields, + ) + }, platform_version, )? - .ok_or(Error::Query( - QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + .ok_or_else(|| match self.resolved_time_range_fields.first() { + // A time-range query is only servable by the index that + // buckets the field, 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 + // that does also covers the rest of the query. + Some(field) => { + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "a time-range query on \"{}\" requires an index that buckets it AND \ + covers the query's other where and order-by fields; valid indexes \ + are: {:?}", + field, + self.document_type.indexes() + ))) + } + None => 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", ))); } + + // Candidate filtering already guarantees a transform-carrying index is + // only reachable when the equality on its source came from + // IN_TIME_RANGE resolution. What it cannot rule out is a query that + // carries that resolved equality AND some other shape on the same + // source — a range, an IN, or an ordering — riding along: those walk + // overlapping bucket keys and return each document up to + // `overlap_factor` times with a perfectly valid proof. Reject them + // here rather than serve a provably wrong answer. The + // `!has_equality_on_source` arm is defensive: resolution always + // pushes the equality, so reaching it means the provenance and the + // clauses disagree. This runs identically on the server and in proof + // verification. + if let Some(transform) = &index.time_range { + let source = transform.source.as_str(); + 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() + .map(|c| c.field == source) + .unwrap_or(false) + || self + .internal_clauses + .in_clauses + .iter() + .any(|c| c.field == source); + let orders_on_source = self.order_by.contains_key(source); + if !has_equality_on_source || range_or_in_on_source || orders_on_source { + return Err(Error::Query(QuerySyntaxError::Unsupported(format!( + "the index on \"{}\" 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", + source + )))); + } + } + Ok(index) } @@ -2390,6 +2710,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; let path_query = query_asc @@ -2770,6 +3091,47 @@ 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; + + let resolved = vec!["$createdAt".to_string()]; + 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 @@ -2823,6 +3185,7 @@ mod tests { start_at: Some([3u8; 32]), start_at_included: false, block_time_ms: None, + resolved_time_range_fields: vec![], }; // Create a document that we are starting at, which may be missing 'transactionIndex' @@ -2938,6 +3301,7 @@ mod tests { start_at: None, start_at_included: false, block_time_ms: None, + resolved_time_range_fields: 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..7e69a445f21 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 @@ -292,6 +292,16 @@ 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 !crate::query::index_admissible_for_resolved_time_range( + index, + &self.resolved_time_range_fields, + ) { + 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/verify/document/verify_proof/mod.rs b/packages/rs-drive/src/verify/document/verify_proof/mod.rs index 46cde078b47..301ebddad6b 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_range_fields: 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..20e2447fd24 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_range_fields: 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..2059c084305 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_range_fields: vec![], }; let result = diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index 92ba675e11e..99629e4bb04 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_range_fields: 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_range_fields: vec![], }; for protocol_version in [13u32, 14u32] { 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 4286f9fb97d..331a04df1c0 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -513,6 +513,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 5ceb6fceaf9..cd9bb8e5f17 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -1,6 +1,7 @@ pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct SystemLimits { @@ -69,6 +70,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 f36f5dd05f8..6c9025fbfd1 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -40,4 +40,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 1fcb2205aa0..5ebf2350076 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -28,4 +28,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 99472f10562..3dfdfddc358 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -30,4 +30,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 new file mode 100644 index 00000000000..b8f985249fb --- /dev/null +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -0,0 +1,35 @@ +use crate::version::system_limits::SystemLimits; + +/// System limits for protocol version 14 and above. +/// +/// Identical to [`super::v3::SYSTEM_LIMITS_V3`] except that +/// `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 + // Use the protocol's existing data-contract schema-depth ceiling as the conservative + // instance budget, bounding pre-schema work well above known document requirements. + max_document_value_depth: Some(256), + max_state_transition_size: 20480, //20 KiB + // Load-bearing for state correctness, not just for throughput — see + // SystemLimits::max_transitions_in_documents_batch and SYSTEM_LIMITS_V1. + max_transitions_in_documents_batch: 1, + withdrawal_transactions_per_block_limit: 4, + retry_signing_expired_withdrawal_documents_per_block_limit: 1, + max_withdrawal_amount: 50_000_000_000_000, //500 Dash + min_withdrawal_amount: 1_000_000, //1000 duffs (raised from 190 in v12) + max_contract_group_size: 256, + max_token_redemption_cycles: 128, + // NOTE: the Halo 2 proof grows with the action count (~2,273 B/action on + // top of the 408 B serialized action), so a transition's on-wire size is + // ~2,681 B per action + ~2,930 B fixed (measured: 2 actions → 8,294 B, + // 6 → 19,018 B). The effective per-transition action bound under the + // 20 KiB `max_state_transition_size` is therefore 6, NOT this cap — 16 + // 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 cfa1f2a0efc..321785c7594 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -25,12 +25,12 @@ use crate::version::drive_versions::v9::DRIVE_VERSION_V9; use crate::version::fee::v2::FEE_VERSION2; use crate::version::protocol_version::PlatformVersion; use crate::version::system_data_contract_versions::v3::SYSTEM_DATA_CONTRACT_VERSIONS_V3; -use crate::version::system_limits::v3::SYSTEM_LIMITS_V3; +use crate::version::system_limits::v4::SYSTEM_LIMITS_V4; use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 hosts three consensus changes: +/// v14 hosts four 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 @@ -66,6 +66,19 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// the contest was created on — which halts the chain when that poll /// ends — or open a contest for a document that is not a contested /// resource at all. +/// 4. **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. A document is +/// stored once per containing bucket (the v2 insert/delete and v1 +/// update walkers carry the fan-out; the per-document write +/// amplification is capped by `SystemLimits:: +/// max_time_range_overlap_factor`), and the v1 `getDocuments` handler +/// resolves the new `IN_TIME_RANGE` operator 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 @@ -74,15 +87,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 and the -/// index-reorder schema-compatibility fix; 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 and the index-reorder schema-compatibility fix; everything +/// else matches v13: /// /// * `CONTRACT_VERSIONS_V6` points `document_type_schema` at the v3 document /// meta-schema, which hosts the ranked index keywords -/// (`rankedCountable` / `rankedSummable` / `rankedAverageable`). v13 keeps -/// validating against meta-schema v2, where those keys are rejected as -/// unknown properties, so a pre-v14 contract cannot smuggle them in. +/// (`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 @@ -94,7 +109,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 @@ -117,10 +133,13 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// is rejected. v13 keeps the v9 table and therefore keeps /// accepting all of these, so replay of pre-upgrade blocks is unchanged. /// -/// 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 @@ -129,7 +148,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { methods: DRIVE_ABCI_METHOD_VERSIONS_V9, validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2, - 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 { @@ -139,7 +158,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 and timeRange keywords document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, @@ -150,7 +169,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_V3, + system_limits: SYSTEM_LIMITS_V4, // changed: time-range overlap-factor cap (24) consensus: ConsensusVersions { tenderdash_consensus_version: 1, }, 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 36e34dd3556..b72153b0bb2 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 68ff854db93..2e727c97d5f 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 @@ -446,6 +446,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 { @@ -488,6 +489,7 @@ impl IdentityWallet { value: Value::Text(normalized), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], @@ -569,6 +571,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![], @@ -1266,6 +1269,7 @@ impl IdentityWallet { value: Value::Identifier(source_document_id.to_buffer()), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![OrderClause { 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 b5582883845..d4315631f7c 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/profile.rs @@ -744,6 +744,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![], @@ -777,6 +778,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/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/documents/average_proof_helpers.rs b/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs index ec73d18fba0..2df7521a0e8 100644 --- a/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs +++ b/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs @@ -96,11 +96,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. + let resolved_time_range_fields = + super::document_query::resolve_time_range_clauses_with_metadata_time( + &mut request, + mtd.time_ms, + )?; + + // Same provenance-vs-shape contract the server dispatchers enforce: a + // resolved field may only carry the single equality its resolution + // produced. A response accepting any other shape did not come from an + // honest prover, so reject before mode detection can route on it. + drive::query::validate_resolved_time_range_clause_shapes( + &request.where_clauses, + &resolved_time_range_fields, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("invalid time range query shape: {}", e), + })?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -110,14 +142,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 +198,7 @@ pub(super) fn verify_average_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_range_fields, ) .filter(|idx| idx.range_countable) .ok_or_else(|| drive_proof_verifier::Error::RequestError { @@ -189,6 +214,7 @@ pub(super) fn verify_average_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_range_fields, ) .filter(|idx| idx.countable.is_countable()) .ok_or_else(|| drive_proof_verifier::Error::RequestError { diff --git a/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs b/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs index 9b99fcdee48..27fcd77f44c 100644 --- a/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs +++ b/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs @@ -138,11 +138,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. + let resolved_time_range_fields = + super::document_query::resolve_time_range_clauses_with_metadata_time( + &mut request, + mtd.time_ms, + )?; + + // Same provenance-vs-shape contract the server dispatchers enforce: a + // resolved field may only carry the single equality its resolution + // produced. A response accepting any other shape did not come from an + // honest prover, so reject before mode detection can route on it. + drive::query::validate_resolved_time_range_clause_shapes( + &request.where_clauses, + &resolved_time_range_fields, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("invalid time range query shape: {}", e), + })?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -152,12 +182,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 +248,7 @@ pub(super) fn verify_count_query( DriveDocumentCountQuery::find_range_countable_index_for_where_clauses( document_type.indexes(), &request.where_clauses, + &resolved_time_range_fields, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "range count requires a `range_countable: true` index whose last \ @@ -234,6 +259,7 @@ pub(super) fn verify_count_query( DriveDocumentCountQuery::find_countable_index_for_where_clauses( document_type.indexes(), &request.where_clauses, + &resolved_time_range_fields, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "prove count requires a `countable: true` index whose properties \ @@ -406,3 +432,292 @@ 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::platform::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 `origin: 0` grid it + /// is itself a bucket start. + const BUCKET_START_MS: u64 = 1_755_000_000_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)`. `origin_seconds` is a + /// parameter because the pre-origin refusal is one of the behaviours + /// under test. + fn trending_contract(origin_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, + "origin": origin_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 resolved_fields = + resolve_time_range_clauses_with_metadata_time(&mut request, metadata_time_ms) + .expect("a metadata time inside an active range resolves"); + + assert_eq!(resolved_fields, vec![CREATED_AT.to_string()]); + 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 before the index's origin belongs to no range at + /// all. The client refuses rather than inventing 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_before_the_index_origin_refuses_to_resolve() { + let origin_seconds = BUCKET_START_MS / 1_000; + let contract = trending_contract(origin_seconds); + let mut request = newest_bucket_query(contract); + + let error = + resolve_time_range_clauses_with_metadata_time(&mut request, BUCKET_START_MS - 1) + .expect_err("a time predating every range has no honest bucket"); + + match error { + drive_proof_verifier::Error::RequestError { error } => assert!( + error.contains("origin"), + "expected the pre-origin 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/rs-sdk/src/platform/documents/document_query.rs b/packages/rs-sdk/src/platform/documents/document_query.rs index 679dcd9f2c9..2f9d68dc7f0 100644 --- a/packages/rs-sdk/src/platform/documents/document_query.rs +++ b/packages/rs-sdk/src/platform/documents/document_query.rs @@ -35,7 +35,7 @@ use drive::query::drive_document_ranked_query::mode_detection::ranked_order_key; use drive::query::{ DriveDocumentQuery, HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, InternalClauses, OrderClause, SelectFunction, SelectProjection, - WhereClause, WhereOperator, + TimeRangeSelector, WhereClause, WhereOperator, }; use drive_proof_verifier::{types::Documents, FromProof}; @@ -74,6 +74,14 @@ pub struct DocumentQuery { pub document_type_name: String, /// `where` clauses for the query pub where_clauses: Vec, + /// Time-range (`IN_TIME_RANGE`) selections — `(field, selector)` pairs 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`]. + #[cfg_attr(feature = "mocks", serde(default))] + pub time_range_clauses: Vec<(String, TimeRangeSelector)>, /// 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 +183,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![], @@ -228,6 +237,24 @@ 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 Platform v3.1+ (v1 wire). + /// + /// Existing time-range selections are preserved. + pub fn with_time_range( + mut self, + field: impl Into, + selector: TimeRangeSelector, + ) -> Self { + self.time_range_clauses.push((field.into(), selector)); + self + } + /// Add order by clause to the query. /// /// Existing order by clauses will be preserved. @@ -457,13 +484,42 @@ 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 before the + // `DriveDocumentQuery` conversion so the engine sees ordinary equality + // clauses. + let mut resolved_time_range_fields = 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_range_fields = + resolve_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_range_fields = resolved_time_range_fields; >::maybe_from_proof_with_metadata( drive_query, @@ -475,6 +531,57 @@ 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 resolved fields — the names whose pushed clause is a bucket +/// equality rather than a raw-timestamp one. Callers must carry them into +/// index selection (`DriveDocumentQuery::resolved_time_range_fields`, or the +/// `resolved_time_range_fields` 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 bucket starts. +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_fields = Vec::with_capacity(time_range_clauses.len()); + for (field, selector) in time_range_clauses { + let resolved = drive::query::resolve_time_range_bucket_clause( + &field, + selector, + 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(resolved); + resolved_fields.push(field); + } + Ok(resolved_fields) +} + /// 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` → @@ -496,6 +603,7 @@ impl TryFromPlatformVersioned for GetDocumentsRequest { data_contract, document_type_name, where_clauses, + time_range_clauses, group_by, having, order_by_clauses, @@ -518,22 +626,32 @@ 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, - ), + 0 => { + if !time_range_clauses.is_empty() { + return Err(Error::Config( + "time range (IN_TIME_RANGE) queries require Platform v3.1+ (the v1 \ + getDocuments wire); the v0 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 => encode_v1( data_contract.id().to_vec(), document_type_name, where_clauses, + time_range_clauses, order_by_clauses, limit, offset, @@ -556,6 +674,7 @@ fn encode_v1( data_contract_id: Vec, document_type: String, where_clauses: Vec, + time_range_clauses: Vec<(String, TimeRangeSelector)>, order_by_clauses: Vec, limit: u32, offset: Option, @@ -564,10 +683,25 @@ 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: field + + // `"newest"`/`"oldest"` text operand. 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 (field, selector) in time_range_clauses { + where_clauses.push(ProtoWhereClause { + field, + operator: ProtoWhereOperator::InTimeRange as i32, + value: Some(ProtoDocumentFieldValue { + variant: Some(document_field_value::Variant::Text( + selector.as_str().to_string(), + )), + }), + }); + } let order_by = order_by_clauses .into_iter() .map(order_clause_to_proto) @@ -736,13 +870,14 @@ impl<'a> From<&'a DriveDocumentQuery<'a>> for DocumentQuery { }; Self { - // `DriveDocumentQuery` has no SELECT/GROUP BY/HAVING + // `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, @@ -772,13 +907,14 @@ impl<'a> From> for DocumentQuery { }; Self { - // `DriveDocumentQuery` has no SELECT/GROUP BY/HAVING + // `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, @@ -793,6 +929,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 @@ -867,6 +1019,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_range_fields: vec![], }; Ok(query) diff --git a/packages/rs-sdk/src/platform/documents/having_proof_helpers.rs b/packages/rs-sdk/src/platform/documents/having_proof_helpers.rs index d989c2ab254..cac6fa3511f 100644 --- a/packages/rs-sdk/src/platform/documents/having_proof_helpers.rs +++ b/packages/rs-sdk/src/platform/documents/having_proof_helpers.rs @@ -88,11 +88,30 @@ 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 into a where clause before + // the shape check, exactly as the server does before routing: a + // having-range query must have no where clauses, so a resolved + // selection is rejected here the same way the server rejects it — + // without this, the verifier would accept a query shape the server + // refuses. The resolved-field list is discarded: nothing survives the + // non-empty-where rejection to consume it. + super::document_query::resolve_time_range_clauses_with_metadata_time( + &mut request, + mtd.time_ms, + )?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -102,12 +121,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/rs-sdk/src/platform/documents/ranked_proof_helpers.rs b/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs index 1aa2fb817eb..6b858b29b3e 100644 --- a/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs +++ b/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs @@ -116,11 +116,33 @@ 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 into + // concrete bucket-equality clauses before the shape check reads + // `request.where_clauses` — the same invariant the count/sum/average + // helpers follow. The ranked surface rejects where clauses today, so a + // ranked + time-range query fails the shape check below with the same + // "no where clauses" error the server's router produces (rather than + // passing the local pre-flight and dying server-side); if ranked routing + // ever grows an equality prefix, resolution is already in place. + // The resolved-field list is discarded: the ranked picker excludes + // bucketed indexes outright, so there is nothing for it to pin. + super::document_query::resolve_time_range_clauses_with_metadata_time( + &mut request, + mtd.time_ms, + )?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -130,12 +152,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/rs-sdk/src/platform/documents/sum_proof_helpers.rs b/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs index 17005e06301..64f54d88e25 100644 --- a/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs +++ b/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs @@ -89,11 +89,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. + let resolved_time_range_fields = + super::document_query::resolve_time_range_clauses_with_metadata_time( + &mut request, + mtd.time_ms, + )?; + + // Same provenance-vs-shape contract the server dispatchers enforce: a + // resolved field may only carry the single equality its resolution + // produced. A response accepting any other shape did not come from an + // honest prover, so reject before mode detection can route on it. + drive::query::validate_resolved_time_range_clause_shapes( + &request.where_clauses, + &resolved_time_range_fields, + ) + .map_err(|e| drive_proof_verifier::Error::RequestError { + error: format!("invalid time range query shape: {}", e), + })?; + let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -103,14 +135,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 +195,7 @@ pub(super) fn verify_sum_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_range_fields, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "prove range SUM requires a `rangeSummable: true` index whose last \ @@ -183,6 +208,7 @@ pub(super) fn verify_sum_query( document_type.indexes(), &request.where_clauses, &sum_property, + &resolved_time_range_fields, ) .ok_or_else(|| drive_proof_verifier::Error::RequestError { error: "prove SUM requires a `summable: \"\"` index whose properties \ diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 4d6ba1f660f..03b6bced0d7 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -429,6 +429,7 @@ impl Sdk { value: Value::Text(normalized_label), }, ], + time_range_clauses: vec![], group_by: vec![], having: vec![], order_by_clauses: vec![], @@ -488,6 +489,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/tests/fetch/document.rs b/packages/rs-sdk/tests/fetch/document.rs index df3b3da576f..058a87e16d9 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_range_fields: 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..fd7d1b1b1ee 100644 --- a/packages/wasm-drive-verify/src/document/verify_proof.rs +++ b/packages/wasm-drive-verify/src/document/verify_proof.rs @@ -108,6 +108,7 @@ pub fn verify_document_proof( start_at: start_at_bytes, start_at_included, block_time_ms, + resolved_time_range_fields: 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..58f3e7b4099 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,7 @@ pub fn verify_document_proof_keep_serialized( start_at: start_at_bytes, start_at_included, block_time_ms, + resolved_time_range_fields: 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..29a4fb6dcc8 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,7 @@ pub fn verify_start_at_document_in_proof( start_at: start_at_bytes, start_at_included, block_time_ms, + resolved_time_range_fields: 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..fb1d67f0a6f 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, TimeRangeSelector, WhereClause, WhereOperator}; use drive_proof_verifier::types::DocumentHistory; use drive_proof_verifier::{DocumentSplitAverages, DocumentSplitCounts, DocumentSplitSums}; use js_sys::{BigInt, Map}; @@ -119,6 +119,20 @@ 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. v1 / Platform v3.1+ only. + * + * - `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). + * @default [] + */ + timeRange?: { field: string; selector: "newest" | "oldest" }[]; } /** @@ -195,6 +209,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 +261,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 +296,13 @@ async fn build_documents_query( } } + if let Some(time_range_values) = time_range { + for clause_json in time_range_values.iter() { + let (field, selector) = parse_time_range_clause(clause_json)?; + query = 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 +482,33 @@ fn parse_where_clause(json_clause: &JsonValue) -> Result Result<(String, TimeRangeSelector), WasmSdkError> { + let object = json_clause.as_object().ok_or_else(|| { + WasmSdkError::invalid_argument("timeRange clause must be an object { field, selector }") + })?; + 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\"", + ) + })?; + Ok((field, selector)) +} + /// Parse JSON order by clause into OrderClause fn parse_order_clause(json_clause: &JsonValue) -> Result { let clause_array = json_clause