From 83697756575836c87f96bb6d4ab55696e8f49de3 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 10:38:25 -0500 Subject: [PATCH 01/12] test: regression test for contestedResources start_index_values proof bug VotePollsByDocumentTypeQuery with start_index_values=["dash"] on the DPNS parentNameAndLabel index produces an invalid GroveDB proof. The proof is missing data for the query range. Without start_index_values the query works and returns top-level keys. Reproduced on mainnet 2026-04-21. --- .../rs-sdk/tests/fetch/contested_resource.rs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 0e606007c95..2e6aa3b63f1 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -436,3 +436,99 @@ pub async fn check_mn_voting_prerequisites(cfg: &Config) -> Result<(), Vec { + let msg = e.to_string(); + tracing::error!(%msg, "Confirmed: start_index_values proof verification failure"); + assert!( + msg.contains("Proof is missing data for query range") + || msg.contains("invalid proof"), + "Expected proof verification error, got: {}", + msg + ); + } + Ok(ref resources) => { + // If this branch is reached, the platform bug has been fixed. + // Update this test to always expect Ok once the fix lands. + tracing::info!( + ?resources, + "start_index_values query now succeeds — bug is fixed!" + ); + assert!( + !resources.0.is_empty(), + "expected contested labels under 'dash'" + ); + } + } +} From 7571f9000bed2dc180f91472065e4079b157eff9 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 10:41:25 -0500 Subject: [PATCH 02/12] test: regression test for contestedResources start_index_values proof bug VotePollsByDocumentTypeQuery with start_index_values=["dash"] on the DPNS parentNameAndLabel index produces an invalid GroveDB proof when limit is None. Adding any explicit limit (e.g. 100) works around the bug. The proof error is: 'Proof is missing data for query range. Encountered unexpected node type: KVHash(...)'. Reproduced on mainnet 2026-04-21. --- .../src/from_request.rs | 56 ++++++++++++++++++- .../rs-sdk/tests/fetch/contested_resource.rs | 54 +++++++----------- 2 files changed, 74 insertions(+), 36 deletions(-) diff --git a/packages/rs-drive-proof-verifier/src/from_request.rs b/packages/rs-drive-proof-verifier/src/from_request.rs index 2a691ec5e3d..1dd6d1fca77 100644 --- a/packages/rs-drive-proof-verifier/src/from_request.rs +++ b/packages/rs-drive-proof-verifier/src/from_request.rs @@ -30,6 +30,12 @@ use drive::query::{ use crate::Error; const BINCODE_CONFIG: dpp::bincode::config::Configuration = dpp::bincode::config::standard(); +/// Default contested-resources limit applied by Platform when `count` is omitted. +/// +/// The proof verifier must mirror this behavior; otherwise it can reconstruct an +/// unbounded query from a request that the server actually executed with the +/// default limit, which makes valid proofs look truncated. +const DEFAULT_CONTESTED_RESOURCES_LIMIT: u16 = 100; /// Convert a gRPC request into a query object. /// @@ -321,7 +327,11 @@ impl TryFromRequest for VotePollsByDocumentTypeQue .transpose()?, start_index_values: bincode_decode_values(req.start_index_values.iter())?, end_index_values: bincode_decode_values(req.end_index_values.iter())?, - limit: req.count.map(|v| v as u16), + limit: Some( + req.count + .unwrap_or(DEFAULT_CONTESTED_RESOURCES_LIMIT as u32) + as u16, + ), order_ascending: req.order_ascending, }, }; @@ -332,7 +342,7 @@ impl TryFromRequest for VotePollsByDocumentTypeQue Ok(GetContestedResourcesRequestV0 { prove: true, contract_id: self.contract_id.to_vec(), - count: self.limit.map(|v| v as u32), + count: Some(self.limit.unwrap_or(DEFAULT_CONTESTED_RESOURCES_LIMIT) as u32), document_type_name: self.document_type_name.clone(), end_index_values: bincode_encode_values(&self.end_index_values)?, start_index_values: bincode_encode_values(&self.start_index_values)?, @@ -642,6 +652,48 @@ mod tests { assert_eq!(roundtripped, id); } + #[test] + fn test_contested_resources_request_defaults_limit_when_encoding() { + let query = VotePollsByDocumentTypeQuery { + contract_id: Identifier::from_bytes(&[1u8; 32]).unwrap(), + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + start_index_values: vec![Value::Text("dash".to_string())], + end_index_values: vec![], + start_at_value: None, + limit: None, + order_ascending: true, + }; + + let request = query.try_to_request().expect("request should encode"); + let proto::get_contested_resources_request::Version::V0(v0) = + request.version.expect("request should contain a version"); + + assert_eq!(v0.count, Some(DEFAULT_CONTESTED_RESOURCES_LIMIT as u32)); + } + + #[test] + fn test_contested_resources_request_defaults_limit_when_decoding() { + let request: GetContestedResourcesRequest = GetContestedResourcesRequestV0 { + prove: true, + contract_id: Identifier::from_bytes(&[2u8; 32]).unwrap().to_vec(), + count: None, + document_type_name: "domain".to_string(), + end_index_values: vec![], + start_index_values: bincode_encode_values([&Value::Text("dash".to_string())]) + .expect("start index values should encode"), + index_name: "parentNameAndLabel".to_string(), + order_ascending: true, + start_at_value_info: None, + } + .into(); + + let query = + VotePollsByDocumentTypeQuery::try_from_request(request).expect("request should decode"); + + assert_eq!(query.limit, Some(DEFAULT_CONTESTED_RESOURCES_LIMIT)); + } + // --------------------------------------------------------------- // Error path: SingleDocumentByContender is rejected in try_to_request // --------------------------------------------------------------- diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 2e6aa3b63f1..aefcc309c81 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -437,8 +437,8 @@ pub async fn check_mn_voting_prerequisites(cfg: &Config) -> Result<(), Vec Result<(), Vec Result<(), Vec| VotePollsByDocumentTypeQuery { contract_id: cfg.existing_data_contract_id, document_type_name: cfg.existing_document_type_name.clone(), index_name: "parentNameAndLabel".to_string(), start_at_value: None, - start_index_values: vec![], + start_index_values: vec![Value::Text("dash".to_string())], end_index_values: vec![], - limit: None, + limit, order_ascending: true, }; - let top_level = ContestedResource::fetch_many(&sdk, query_no_prefix) + // 1. With limit → works fine. + let with_limit = ContestedResource::fetch_many(&sdk, make_query(Some(100))) .await - .expect("query without start_index_values should succeed"); - tracing::info!(?top_level, "Top-level values (no start_index_values)"); - assert!( - !top_level.0.is_empty(), - "expected at least one top-level value (e.g. 'dash')" - ); - - // 2. With start_index_values = ["dash"] → should return contested labels - // but currently fails proof verification. - let query_with_prefix = VotePollsByDocumentTypeQuery { - contract_id: cfg.existing_data_contract_id, - document_type_name: cfg.existing_document_type_name.clone(), - index_name: "parentNameAndLabel".to_string(), - start_at_value: None, - start_index_values: vec![Value::Text("dash".to_string())], - end_index_values: vec![], - limit: None, - order_ascending: true, - }; + .expect("query with start_index_values + limit should succeed"); + tracing::info!(count = with_limit.0.len(), "With limit: OK"); + assert!(!with_limit.0.is_empty(), "expected contested labels"); - let result = ContestedResource::fetch_many(&sdk, query_with_prefix).await; + // 2. Without limit → proof verification fails. + let result = ContestedResource::fetch_many(&sdk, make_query(None)).await; match result { Err(ref e) => { let msg = e.to_string(); - tracing::error!(%msg, "Confirmed: start_index_values proof verification failure"); + tracing::error!(%msg, "Confirmed: no-limit proof verification failure"); assert!( msg.contains("Proof is missing data for query range") || msg.contains("invalid proof"), @@ -522,8 +508,8 @@ async fn contested_resources_start_index_values_proof_verification_failure() { // If this branch is reached, the platform bug has been fixed. // Update this test to always expect Ok once the fix lands. tracing::info!( - ?resources, - "start_index_values query now succeeds — bug is fixed!" + count = resources.0.len(), + "No-limit query now succeeds — bug is fixed!" ); assert!( !resources.0.is_empty(), From 8016f640e9126765e078bf9015c7ac6105f96cb0 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 11:49:17 -0500 Subject: [PATCH 03/12] test(repro): add default limit proof mismatch script --- .../repro-default-limit-proof-mismatches.mjs | 743 ++++++++++++++++++ 1 file changed, 743 insertions(+) create mode 100644 scripts/repro-default-limit-proof-mismatches.mjs diff --git a/scripts/repro-default-limit-proof-mismatches.mjs b/scripts/repro-default-limit-proof-mismatches.mjs new file mode 100644 index 00000000000..851739e0de3 --- /dev/null +++ b/scripts/repro-default-limit-proof-mismatches.mjs @@ -0,0 +1,743 @@ +#!/usr/bin/env node + +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +/* + * Reproduce proof-verification mismatches caused by omitted limits. + * + * The bug shape is: + * - Platform applies a default limit when count/limit is omitted + * - The proof verifier rebuilds the request as if no limit was applied + * - The same query succeeds when an explicit limit is supplied + * + * Usage: + * node scripts/repro-default-limit-proof-mismatches.mjs + * + * If you are running from this monorepo with Yarn Plug'n'Play, use the + * package-aware runtime instead of plain Node. For example: + * yarn node scripts/repro-default-limit-proof-mismatches.mjs + * + * Optional environment variables: + * EVO_SDK_IMPORT + * Alternate module specifier for EvoSDK. If omitted, the script tries: + * 1. "@dashevo/evo-sdk" + * 2. the local workspace build at packages/js-evo-sdk/dist/sdk.js + * + * NETWORK + * mainnet | testnet | local. Defaults to mainnet. + * + * TRUSTED + * true | false. Defaults to true. + * + * DEFAULT_LIMIT + * Explicit limit to use for the control query. Defaults to 100. + * + * DPNS_CONTRACT_ID + * Defaults to the DPNS mainnet contract used in the earlier repro. + * + * DPNS_DOCUMENT_TYPE + * Defaults to "domain". + * + * DPNS_INDEX + * Defaults to "parentNameAndLabel". + * + * CONTESTED_PARENT + * Defaults to "dash". + * + * VOTE_STATE_LABEL + * Optional explicit label for contestedResourceVoteState repro. + * + * VOTERS_LABEL + * VOTERS_CONTESTANT_ID + * Optional explicit fixture for contestedResourceVotersForIdentity repro. + * + * IDENTITY_VOTES_IDENTITY_ID + * Optional explicit fixture for contestedResourceIdentityVotes repro. + * + * VOTE_POLLS_START_MS + * VOTE_POLLS_END_MS + * Optional explicit time range for votePollsByEndDate repro. + * + * GROUP_INFOS_CONTRACT_ID + * Optional fixture for group.infos repro. + * + * GROUP_ACTIONS_CONTRACT_ID + * GROUP_ACTIONS_POSITION + * GROUP_ACTIONS_STATUS + * Optional fixture for group.actions repro. + * + * TOKEN_DISTRIBUTIONS_TOKEN_ID + * Reserved for a future EvoSDK token pre-programmed distributions repro. + * The current JS EvoSDK does not expose this query yet, so the script + * reports it as skipped. + */ + +const DEFAULT_DPNS_CONTRACT_ID = + process.env.DPNS_CONTRACT_ID ?? 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'; +const DEFAULT_DPNS_DOCUMENT_TYPE = process.env.DPNS_DOCUMENT_TYPE ?? 'domain'; +const DEFAULT_DPNS_INDEX = process.env.DPNS_INDEX ?? 'parentNameAndLabel'; +const DEFAULT_PARENT = process.env.CONTESTED_PARENT ?? 'dash'; +const DEFAULT_LIMIT = Number(process.env.DEFAULT_LIMIT ?? '100'); +const NETWORK = process.env.NETWORK ?? 'mainnet'; +const TRUSTED = (process.env.TRUSTED ?? 'true').toLowerCase() !== 'false'; +const localSdkImport = new URL('../packages/js-evo-sdk/dist/sdk.js', import.meta.url).href; +const EVO_SDK_IMPORT = process.env.EVO_SDK_IMPORT ?? null; + +const nowMs = Date.now(); +const defaultVotePollsStartMs = Number(process.env.VOTE_POLLS_START_MS ?? '0'); +const defaultVotePollsEndMs = Number( + process.env.VOTE_POLLS_END_MS ?? String(nowMs + 365 * 24 * 60 * 60 * 1000), +); + +function printHeading(title) { + console.log(`\n=== ${title} ===`); +} + +function shorten(value, max = 140) { + const text = typeof value === 'string' ? value : JSON.stringify(value); + if (text.length <= max) { + return text; + } + return `${text.slice(0, max - 3)}...`; +} + +function normalizeError(error) { + if (!error) { + return 'Unknown error'; + } + return error?.message ?? String(error); +} + +function countResult(result) { + if (Array.isArray(result)) { + return result.length; + } + if (result instanceof Map) { + return result.size; + } + if (typeof result?.size === 'number') { + return result.size; + } + if (typeof result?.length === 'number') { + return result.length; + } + return null; +} + +function summarizeResult(result) { + if (Array.isArray(result)) { + return shorten(result.slice(0, 5).map(stringifySdkValue)); + } + if (result instanceof Map) { + return shorten( + Array.from(result.entries()) + .slice(0, 5) + .map(([key, value]) => [key, stringifySdkValue(value)]), + ); + } + return shorten(stringifySdkValue(result)); +} + +function stringifySdkValue(value) { + if (value == null) { + return value; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (Array.isArray(value)) { + return value.map(stringifySdkValue); + } + if (value instanceof Uint8Array) { + return Array.from(value); + } + if (typeof value?.toJSON === 'function') { + try { + return value.toJSON(); + } catch { + // ignore + } + } + if (typeof value?.toString === 'function' && value.toString !== Object.prototype.toString) { + try { + const text = value.toString(); + if (text && text !== '[object Object]') { + return text; + } + } catch { + // ignore + } + } + return value; +} + +function identifierToString(id) { + if (!id) { + return null; + } + if (typeof id === 'string') { + return id; + } + if (typeof id.toString === 'function') { + return id.toString(); + } + return String(id); +} + +async function buildSdk(EvoSDK) { + const builderName = `${NETWORK}${TRUSTED ? 'Trusted' : ''}`; + if (typeof EvoSDK[builderName] !== 'function') { + throw new Error(`Unsupported NETWORK/TRUSTED combination: ${builderName}`); + } + + const sdk = await EvoSDK[builderName](); + await sdk.connect(); + return sdk; +} + +function resolveBareImportFromNodeModules(specifier) { + let currentDir = process.cwd(); + + while (true) { + const packageDir = path.join(currentDir, 'node_modules', specifier); + const packageJsonPath = path.join(packageDir, 'package.json'); + + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + const exportEntry = packageJson.exports?.['.']; + const relativeEntry = + (typeof exportEntry === 'string' && exportEntry) + || exportEntry?.import + || packageJson.module + || packageJson.main; + + if (!relativeEntry) { + throw new Error(`Could not determine entry point from ${packageJsonPath}`); + } + + return pathToFileURL(path.join(packageDir, relativeEntry)).href; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + + return null; +} + +async function loadEvoSdk() { + const candidates = EVO_SDK_IMPORT + ? [EVO_SDK_IMPORT] + : ['@dashevo/evo-sdk', localSdkImport]; + + const failures = []; + for (const specifier of candidates) { + try { + let target = specifier; + if ( + !specifier.startsWith('.') + && !specifier.startsWith('/') + && !specifier.startsWith('file:') + ) { + target = resolveBareImportFromNodeModules(specifier) ?? specifier; + } + + const mod = await import(target); + if (mod?.EvoSDK) { + return mod.EvoSDK; + } + failures.push(`${specifier}: imported, but no EvoSDK export was found`); + } catch (error) { + failures.push(`${specifier}: ${normalizeError(error)}`); + } + } + + throw new Error( + [ + 'Unable to import EvoSDK.', + ...failures.map((failure) => `- ${failure}`), + "If you are in this monorepo, run via `yarn node` so Plug'n'Play dependencies resolve.", + 'If you want the published package, install `@dashevo/evo-sdk` and optionally set EVO_SDK_IMPORT=@dashevo/evo-sdk.', + ].join('\n'), + ); +} + +async function runPair(name, buildNoLimit, buildWithLimit) { + const record = { + name, + noLimit: null, + explicitLimit: null, + conclusion: null, + }; + + const cases = [ + ['noLimit', buildNoLimit], + ['explicitLimit', buildWithLimit], + ]; + + for (const [key, build] of cases) { + try { + const result = await build(); + record[key] = { + ok: true, + count: countResult(result), + summary: summarizeResult(result), + }; + } catch (error) { + record[key] = { + ok: false, + error: normalizeError(error), + }; + } + } + + if (record.noLimit?.ok === false && record.explicitLimit?.ok === true) { + record.conclusion = 'CONFIRMED_DEFAULT_LIMIT_MISMATCH'; + } else if (record.noLimit?.ok === true && record.explicitLimit?.ok === true) { + record.conclusion = 'NO_FAILURE_WITH_CURRENT_FIXTURE'; + } else if (record.noLimit?.ok === false && record.explicitLimit?.ok === false) { + record.conclusion = 'FIXTURE_OR_ENDPOINT_FAILED_BOTH_WAYS'; + } else { + record.conclusion = 'INCONCLUSIVE'; + } + + return record; +} + +function printRecord(record) { + console.log(`\n[${record.conclusion}] ${record.name}`); + + for (const key of ['noLimit', 'explicitLimit']) { + const value = record[key]; + if (!value) { + console.log(` ${key}: skipped`); + continue; + } + + if (value.ok) { + const count = value.count == null ? 'n/a' : value.count; + console.log(` ${key}: ok, count=${count}, sample=${value.summary}`); + } else { + console.log(` ${key}: error=${value.error}`); + } + } +} + +function getTokenPreProgrammedDistributionsMethod(sdk) { + const candidateNames = [ + 'preProgrammedDistributions', + 'tokenPreProgrammedDistributions', + 'getPreProgrammedDistributions', + ]; + + for (const name of candidateNames) { + if (typeof sdk?.tokens?.[name] === 'function') { + return sdk.tokens[name].bind(sdk.tokens); + } + } + + return null; +} + +async function autoDiscoverContestedLabel(sdk) { + const labels = await sdk.group.contestedResources({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + startIndexValues: [DEFAULT_PARENT], + limit: DEFAULT_LIMIT, + orderAscending: true, + }); + + const explicit = process.env.VOTE_STATE_LABEL; + if (explicit) { + return { label: explicit, labels }; + } + + const activeEntries = await sdk.voting.votePollsByEndDate({ + startTimeMs: defaultVotePollsStartMs, + startTimeIncluded: true, + endTimeMs: defaultVotePollsEndMs, + endTimeIncluded: true, + orderAscending: true, + limit: DEFAULT_LIMIT, + }); + + for (const entry of activeEntries) { + for (const poll of entry.votePolls) { + const json = poll.toJSON?.(); + const values = json?.contestedDocumentResourceVotePoll?.indexValues; + if (Array.isArray(values) && values[0] === DEFAULT_PARENT && typeof values[1] === 'string') { + return { label: values[1], labels }; + } + } + } + + return { label: labels[0] ?? null, labels }; +} + +async function autoDiscoverContestantId(sdk, label) { + if (process.env.VOTERS_CONTESTANT_ID) { + return process.env.VOTERS_CONTESTANT_ID; + } + + if (!label) { + return null; + } + + const state = await sdk.voting.contestedResourceVoteState({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + indexValues: [DEFAULT_PARENT, label], + resultType: 'documentsAndVoteTally', + includeLockedAndAbstaining: true, + limit: DEFAULT_LIMIT, + }); + + const contender = state?.contenders?.[0]; + return identifierToString(contender?.identityId); +} + +async function autoDiscoverIdentityVotesIdentityId(sdk, label, contestantId) { + if (process.env.IDENTITY_VOTES_IDENTITY_ID) { + return process.env.IDENTITY_VOTES_IDENTITY_ID; + } + + if (!label || !contestantId) { + return null; + } + + try { + const voters = await sdk.group.contestedResourceVotersForIdentity({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + indexValues: [DEFAULT_PARENT, label], + contestantId, + orderAscending: true, + limit: DEFAULT_LIMIT, + }); + + return identifierToString(voters?.[0]); + } catch { + return null; + } +} + +async function main() { + const EvoSDK = await loadEvoSdk(); + const sdk = await buildSdk(EvoSDK); + + try { + printHeading(`Connected to ${NETWORK} (${TRUSTED ? 'trusted' : 'untrusted'})`); + console.log(`using explicit control limit = ${DEFAULT_LIMIT}`); + + const records = []; + + records.push( + await runPair( + 'group.contestedResources(startIndexValues=["dash"])', + () => + sdk.group.contestedResources({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + startIndexValues: [DEFAULT_PARENT], + orderAscending: true, + }), + () => + sdk.group.contestedResources({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + startIndexValues: [DEFAULT_PARENT], + orderAscending: true, + limit: DEFAULT_LIMIT, + }), + ), + ); + + const currentEpoch = await sdk.epoch.current(); + const epochIndex = Number(currentEpoch.index ?? currentEpoch); + records.push( + await runPair( + `epoch.evonodesProposedBlocksByRange(epoch=${epochIndex})`, + () => + sdk.epoch.evonodesProposedBlocksByRange({ + epoch: epochIndex, + orderAscending: true, + }), + () => + sdk.epoch.evonodesProposedBlocksByRange({ + epoch: epochIndex, + orderAscending: true, + limit: DEFAULT_LIMIT, + }), + ), + ); + + const { label } = await autoDiscoverContestedLabel(sdk); + if (label) { + records.push( + await runPair( + `voting.contestedResourceVoteState(label=${label})`, + () => + sdk.voting.contestedResourceVoteState({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + indexValues: [DEFAULT_PARENT, label], + resultType: 'documentsAndVoteTally', + includeLockedAndAbstaining: true, + }), + () => + sdk.voting.contestedResourceVoteState({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + indexValues: [DEFAULT_PARENT, label], + resultType: 'documentsAndVoteTally', + includeLockedAndAbstaining: true, + limit: DEFAULT_LIMIT, + }), + ), + ); + + const contestantId = await autoDiscoverContestantId(sdk, process.env.VOTERS_LABEL ?? label); + if (contestantId) { + const votersLabel = process.env.VOTERS_LABEL ?? label; + records.push( + await runPair( + `group.contestedResourceVotersForIdentity(label=${votersLabel}, contestantId=${contestantId})`, + () => + sdk.group.contestedResourceVotersForIdentity({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + indexValues: [DEFAULT_PARENT, votersLabel], + contestantId, + orderAscending: true, + }), + () => + sdk.group.contestedResourceVotersForIdentity({ + dataContractId: DEFAULT_DPNS_CONTRACT_ID, + documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, + indexName: DEFAULT_DPNS_INDEX, + indexValues: [DEFAULT_PARENT, votersLabel], + contestantId, + orderAscending: true, + limit: DEFAULT_LIMIT, + }), + ), + ); + + const identityVotesIdentityId = await autoDiscoverIdentityVotesIdentityId( + sdk, + votersLabel, + contestantId, + ); + + if (identityVotesIdentityId) { + records.push( + await runPair( + `voting.contestedResourceIdentityVotes(identityId=${identityVotesIdentityId})`, + () => + sdk.voting.contestedResourceIdentityVotes({ + identityId: identityVotesIdentityId, + orderAscending: true, + }), + () => + sdk.voting.contestedResourceIdentityVotes({ + identityId: identityVotesIdentityId, + orderAscending: true, + limit: DEFAULT_LIMIT, + }), + ), + ); + } else { + records.push({ + name: 'voting.contestedResourceIdentityVotes', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_IDENTITY_FIXTURE', + }); + } + } else { + records.push({ + name: 'group.contestedResourceVotersForIdentity', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_CONTESTANT_FIXTURE', + }); + records.push({ + name: 'voting.contestedResourceIdentityVotes', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_IDENTITY_FIXTURE', + }); + } + } else { + records.push({ + name: 'voting.contestedResourceVoteState', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_LABEL_DISCOVERED', + }); + records.push({ + name: 'group.contestedResourceVotersForIdentity', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_CONTESTANT_FIXTURE', + }); + records.push({ + name: 'voting.contestedResourceIdentityVotes', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_IDENTITY_FIXTURE', + }); + } + + records.push( + await runPair( + `voting.votePollsByEndDate(start=${defaultVotePollsStartMs}, end=${defaultVotePollsEndMs})`, + () => + sdk.voting.votePollsByEndDate({ + startTimeMs: defaultVotePollsStartMs, + startTimeIncluded: true, + endTimeMs: defaultVotePollsEndMs, + endTimeIncluded: true, + orderAscending: true, + }), + () => + sdk.voting.votePollsByEndDate({ + startTimeMs: defaultVotePollsStartMs, + startTimeIncluded: true, + endTimeMs: defaultVotePollsEndMs, + endTimeIncluded: true, + orderAscending: true, + limit: DEFAULT_LIMIT, + }), + ), + ); + + if (process.env.GROUP_INFOS_CONTRACT_ID) { + records.push( + await runPair( + `group.infos(contractId=${process.env.GROUP_INFOS_CONTRACT_ID})`, + () => + sdk.group.infos({ + dataContractId: process.env.GROUP_INFOS_CONTRACT_ID, + }), + () => + sdk.group.infos({ + dataContractId: process.env.GROUP_INFOS_CONTRACT_ID, + limit: DEFAULT_LIMIT, + }), + ), + ); + } else { + records.push({ + name: 'group.infos', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_GROUP_INFOS_CONTRACT_ID', + }); + } + + if ( + process.env.GROUP_ACTIONS_CONTRACT_ID + && process.env.GROUP_ACTIONS_POSITION + && process.env.GROUP_ACTIONS_STATUS + ) { + const groupContractPosition = Number(process.env.GROUP_ACTIONS_POSITION); + records.push( + await runPair( + `group.actions(contractId=${process.env.GROUP_ACTIONS_CONTRACT_ID}, position=${groupContractPosition})`, + () => + sdk.group.actions({ + dataContractId: process.env.GROUP_ACTIONS_CONTRACT_ID, + groupContractPosition, + status: process.env.GROUP_ACTIONS_STATUS, + }), + () => + sdk.group.actions({ + dataContractId: process.env.GROUP_ACTIONS_CONTRACT_ID, + groupContractPosition, + status: process.env.GROUP_ACTIONS_STATUS, + limit: DEFAULT_LIMIT, + }), + ), + ); + } else { + records.push({ + name: 'group.actions', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_GROUP_ACTIONS_FIXTURE', + }); + } + + const tokenDistributionsMethod = getTokenPreProgrammedDistributionsMethod(sdk); + if (!tokenDistributionsMethod) { + records.push({ + name: 'tokens.preProgrammedDistributions', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NOT_EXPOSED_BY_EVO_SDK', + }); + } else if (!process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID) { + records.push({ + name: 'tokens.preProgrammedDistributions', + noLimit: null, + explicitLimit: null, + conclusion: 'SKIPPED_NO_TOKEN_DISTRIBUTIONS_TOKEN_ID', + }); + } else { + records.push( + await runPair( + `tokens.preProgrammedDistributions(tokenId=${process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID})`, + () => + tokenDistributionsMethod({ + tokenId: process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID, + }), + () => + tokenDistributionsMethod({ + tokenId: process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID, + limit: DEFAULT_LIMIT, + }), + ), + ); + } + + printHeading('Results'); + for (const record of records) { + printRecord(record); + } + + const confirmed = records.filter( + (record) => record.conclusion === 'CONFIRMED_DEFAULT_LIMIT_MISMATCH', + ); + printHeading('Summary'); + console.log(`confirmed mismatches: ${confirmed.length}`); + for (const record of confirmed) { + console.log(`- ${record.name}`); + } + } finally { + try { + await sdk.disconnect?.(); + } catch { + // ignore disconnect failures + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); From 169c120fcc5990fe11eb72a753fe75fc38d2ab68 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 11:59:17 -0500 Subject: [PATCH 04/12] fix(proof-verifier): default remaining proved query limits --- .../src/from_request.rs | 200 +++++++++++++++--- packages/rs-drive-proof-verifier/src/lib.rs | 35 +++ packages/rs-drive-proof-verifier/src/proof.rs | 4 +- .../src/proof/groups.rs | 6 +- .../token_pre_programmed_distributions.rs | 11 +- 5 files changed, 218 insertions(+), 38 deletions(-) diff --git a/packages/rs-drive-proof-verifier/src/from_request.rs b/packages/rs-drive-proof-verifier/src/from_request.rs index 1dd6d1fca77..cc363a95db5 100644 --- a/packages/rs-drive-proof-verifier/src/from_request.rs +++ b/packages/rs-drive-proof-verifier/src/from_request.rs @@ -27,16 +27,9 @@ use drive::query::{ VotePollsByEndDateDriveQuery, }; -use crate::Error; +use crate::{proved_request_limit, Error, DEFAULT_QUERY_LIMIT}; const BINCODE_CONFIG: dpp::bincode::config::Configuration = dpp::bincode::config::standard(); -/// Default contested-resources limit applied by Platform when `count` is omitted. -/// -/// The proof verifier must mirror this behavior; otherwise it can reconstruct an -/// unbounded query from a request that the server actually executed with the -/// default limit, which makes valid proofs look truncated. -const DEFAULT_CONTESTED_RESOURCES_LIMIT: u16 = 100; - /// Convert a gRPC request into a query object. /// /// This trait is implemented on Drive queries that can be created from gRPC requests. @@ -92,7 +85,7 @@ impl TryFromRequest for ContestedDocumentV let result = match grpc_request.version.ok_or(Error::EmptyVersion)? { get_contested_resource_vote_state_request::Version::V0(v) => { ContestedDocumentVotePollDriveQuery { - limit: v.count.map(|v| v as u16), + limit: Some(proved_request_limit(v.count)?), vote_poll: ContestedDocumentResourceVotePoll { contract_id: Identifier::from_bytes(&v.contract_id).map_err(|e| { Error::RequestError { @@ -149,7 +142,7 @@ impl TryFromRequest for ContestedDocumentV Ok(proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 { prove:true, contract_id:self.vote_poll.contract_id.to_vec(), - count: self.limit.map(|v| v as u32), + count: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), document_type_name: self.vote_poll.document_type_name.clone(), index_name: self.vote_poll.index_name.clone(), index_values: self.vote_poll.index_values.iter().map(|v| @@ -202,7 +195,7 @@ impl TryFromRequest } })?, offset: None, - limit: value.limit.map(|x| x as u16), + limit: Some(proved_request_limit(value.limit)?), start_at, order_ascending: value.order_ascending, }) @@ -218,7 +211,7 @@ impl TryFromRequest prove: true, identity_id: self.identity_id.to_vec(), offset: self.offset.map(|x| x as u32), - limit: self.limit.map(|x| x as u32), + limit: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), start_at_vote_poll_id_info: self.start_at.map(|(id, included)| { request_v0::StartAtVotePollIdInfo { start_at_poll_identifier: id.to_vec(), @@ -257,7 +250,7 @@ impl TryFromRequest error: format!("cannot decode contestant_id: {}", e), } })?, - limit: v.count.map(|v| v as u16), + limit: Some(proved_request_limit(v.count)?), offset: None, start_at: v .start_at_identifier_info @@ -291,7 +284,7 @@ impl TryFromRequest dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e| Error::RequestError { error: e.to_string()})).collect::,_>>()?, order_ascending: self.order_ascending, - count: self.limit.map(|v| v as u32), + count: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), contestant_id: self.contestant_id.to_vec(), start_at_identifier_info: self.start_at.map(|v| request_v0::StartAtIdentifierInfo{ start_identifier: v.0.to_vec(), @@ -327,11 +320,7 @@ impl TryFromRequest for VotePollsByDocumentTypeQue .transpose()?, start_index_values: bincode_decode_values(req.start_index_values.iter())?, end_index_values: bincode_decode_values(req.end_index_values.iter())?, - limit: Some( - req.count - .unwrap_or(DEFAULT_CONTESTED_RESOURCES_LIMIT as u32) - as u16, - ), + limit: Some(proved_request_limit(req.count)?), order_ascending: req.order_ascending, }, }; @@ -342,7 +331,7 @@ impl TryFromRequest for VotePollsByDocumentTypeQue Ok(GetContestedResourcesRequestV0 { prove: true, contract_id: self.contract_id.to_vec(), - count: Some(self.limit.unwrap_or(DEFAULT_CONTESTED_RESOURCES_LIMIT) as u32), + count: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), document_type_name: self.document_type_name.clone(), end_index_values: bincode_encode_values(&self.end_index_values)?, start_index_values: bincode_encode_values(&self.start_index_values)?, @@ -377,7 +366,7 @@ impl TryFromRequest for VotePollsByEndDateDriveQue end_time: v .end_time_info .map(|v| (v.end_time_ms, v.end_time_included)), - limit: v.limit.map(|v| v as u16), + limit: Some(proved_request_limit(v.limit)?), offset: v.offset.map(|v| v as u16), order_ascending: v.ascending, }, @@ -410,7 +399,7 @@ impl TryFromRequest for VotePollsByEndDateDriveQue end_time_included, } }), - limit: self.limit.map(|v| v as u32), + limit: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), offset: self.offset.map(|v| v as u32), ascending: self.order_ascending, } @@ -482,6 +471,15 @@ mod tests { use dpp::identifier::Identifier; use dpp::platform_value::Value; + fn sample_vote_poll() -> ContestedDocumentResourceVotePoll { + ContestedDocumentResourceVotePoll { + contract_id: Identifier::from_bytes(&[1u8; 32]).unwrap(), + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: vec![Value::Text("dash".to_string())], + } + } + // --------------------------------------------------------------- // Helper: to_bytes32 // --------------------------------------------------------------- @@ -669,7 +667,7 @@ mod tests { let proto::get_contested_resources_request::Version::V0(v0) = request.version.expect("request should contain a version"); - assert_eq!(v0.count, Some(DEFAULT_CONTESTED_RESOURCES_LIMIT as u32)); + assert_eq!(v0.count, Some(DEFAULT_QUERY_LIMIT as u32)); } #[test] @@ -691,7 +689,161 @@ mod tests { let query = VotePollsByDocumentTypeQuery::try_from_request(request).expect("request should decode"); - assert_eq!(query.limit, Some(DEFAULT_CONTESTED_RESOURCES_LIMIT)); + assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + } + + #[test] + fn test_contested_resource_vote_state_request_defaults_limit_when_encoding() { + let query = ContestedDocumentVotePollDriveQuery { + vote_poll: sample_vote_poll(), + result_type: ContestedDocumentVotePollDriveQueryResultType::Documents, + offset: None, + limit: None, + start_at: None, + allow_include_locked_and_abstaining_vote_tally: false, + }; + + let request = query.try_to_request().expect("request should encode"); + let proto::get_contested_resource_vote_state_request::Version::V0(v0) = + request.version.expect("request should contain a version"); + + assert_eq!(v0.count, Some(DEFAULT_QUERY_LIMIT as u32)); + } + + #[test] + fn test_contested_resource_vote_state_request_defaults_limit_when_decoding() { + let request: GetContestedResourceVoteStateRequest = + proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 { + prove: true, + contract_id: sample_vote_poll().contract_id.to_vec(), + count: None, + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: bincode_encode_values([&Value::Text("dash".to_string())]) + .expect("index values should encode"), + result_type: get_contested_resource_vote_state_request_v0::ResultType::Documents.into(), + start_at_identifier_info: None, + allow_include_locked_and_abstaining_vote_tally: false, + } + .into(); + + let query = ContestedDocumentVotePollDriveQuery::try_from_request(request) + .expect("request should decode"); + + assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + } + + #[test] + fn test_contested_resource_identity_votes_request_defaults_limit_when_encoding() { + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id: Identifier::from_bytes(&[9u8; 32]).unwrap(), + offset: None, + limit: None, + start_at: None, + order_ascending: true, + }; + + let request = query.try_to_request().expect("request should encode"); + let proto::get_contested_resource_identity_votes_request::Version::V0(v0) = + request.version.expect("request should contain a version"); + + assert_eq!(v0.limit, Some(DEFAULT_QUERY_LIMIT as u32)); + } + + #[test] + fn test_contested_resource_identity_votes_request_defaults_limit_when_decoding() { + let request: GetContestedResourceIdentityVotesRequest = + proto::get_contested_resource_identity_votes_request::GetContestedResourceIdentityVotesRequestV0 { + prove: true, + identity_id: Identifier::from_bytes(&[8u8; 32]).unwrap().to_vec(), + offset: None, + limit: None, + start_at_vote_poll_id_info: None, + order_ascending: true, + } + .into(); + + let query = ContestedResourceVotesGivenByIdentityQuery::try_from_request(request) + .expect("request should decode"); + + assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + } + + #[test] + fn test_contested_resource_voters_for_identity_request_defaults_limit_when_encoding() { + let query = ContestedDocumentVotePollVotesDriveQuery { + vote_poll: sample_vote_poll(), + contestant_id: Identifier::from_bytes(&[3u8; 32]).unwrap(), + offset: None, + limit: None, + start_at: None, + order_ascending: true, + }; + + let request = query.try_to_request().expect("request should encode"); + let proto::get_contested_resource_voters_for_identity_request::Version::V0(v0) = + request.version.expect("request should contain a version"); + + assert_eq!(v0.count, Some(DEFAULT_QUERY_LIMIT as u32)); + } + + #[test] + fn test_contested_resource_voters_for_identity_request_defaults_limit_when_decoding() { + let request: GetContestedResourceVotersForIdentityRequest = + proto::get_contested_resource_voters_for_identity_request::GetContestedResourceVotersForIdentityRequestV0 { + prove: true, + contract_id: sample_vote_poll().contract_id.to_vec(), + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: bincode_encode_values([&Value::Text("dash".to_string())]) + .expect("index values should encode"), + contestant_id: Identifier::from_bytes(&[4u8; 32]).unwrap().to_vec(), + start_at_identifier_info: None, + count: None, + order_ascending: true, + } + .into(); + + let query = ContestedDocumentVotePollVotesDriveQuery::try_from_request(request) + .expect("request should decode"); + + assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + } + + #[test] + fn test_vote_polls_by_end_date_request_defaults_limit_when_encoding() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, true)), + end_time: Some((2000, false)), + limit: None, + offset: None, + order_ascending: true, + }; + + let request = query.try_to_request().expect("request should encode"); + let proto::get_vote_polls_by_end_date_request::Version::V0(v0) = + request.version.expect("request should contain a version"); + + assert_eq!(v0.limit, Some(DEFAULT_QUERY_LIMIT as u32)); + } + + #[test] + fn test_vote_polls_by_end_date_request_defaults_limit_when_decoding() { + let request: GetVotePollsByEndDateRequest = + proto::get_vote_polls_by_end_date_request::GetVotePollsByEndDateRequestV0 { + prove: true, + start_time_info: None, + end_time_info: None, + limit: None, + offset: None, + ascending: true, + } + .into(); + + let query = + VotePollsByEndDateDriveQuery::try_from_request(request).expect("request should decode"); + + assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); } // --------------------------------------------------------------- diff --git a/packages/rs-drive-proof-verifier/src/lib.rs b/packages/rs-drive-proof-verifier/src/lib.rs index 5790b842a93..5f4375fc0eb 100644 --- a/packages/rs-drive-proof-verifier/src/lib.rs +++ b/packages/rs-drive-proof-verifier/src/lib.rs @@ -21,6 +21,41 @@ pub mod from_request; /// Implementation of unproved verification pub mod unproved; +/// Default query limit applied by Platform when proved paginated requests omit `count` or `limit`. +/// +/// Proof verification must mirror this behavior; otherwise a valid proof for a default-bounded +/// server query is reconstructed locally as an unbounded query and looks truncated. +pub(crate) const DEFAULT_QUERY_LIMIT: u16 = 100; + +/// Parse a proved request's optional `count`/`limit`, applying Platform's default when omitted. +pub(crate) fn proved_request_limit(limit: Option) -> Result { + limit.map_or(Ok(DEFAULT_QUERY_LIMIT), |limit| { + u16::try_from(limit).map_err(|_| Error::RequestError { + error: "query limit exceeds u16::MAX".to_string(), + }) + }) +} + // Needed for #[derive(PlatformSerialize, PlatformDeserialize)] #[cfg(feature = "mocks")] use dpp::serialization; + +#[cfg(test)] +mod tests { + use super::{proved_request_limit, DEFAULT_QUERY_LIMIT}; + + #[test] + fn proved_request_limit_defaults_when_omitted() { + assert_eq!(proved_request_limit(None).unwrap(), DEFAULT_QUERY_LIMIT); + } + + #[test] + fn proved_request_limit_preserves_explicit_value() { + assert_eq!(proved_request_limit(Some(7)).unwrap(), 7); + } + + #[test] + fn proved_request_limit_rejects_u32_overflow() { + assert!(proved_request_limit(Some(u16::MAX as u32 + 1)).is_err()); + } +} diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 8ef12c026c6..b813d7420a3 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -10,7 +10,7 @@ pub mod token_total_supply; use crate::from_request::TryFromRequest; use crate::verify::verify_tenderdash_proof; -use crate::{types::*, ContextProvider, DataContractProvider, Error}; +use crate::{proved_request_limit, types::*, ContextProvider, DataContractProvider, Error}; use dapi_grpc::platform::v0::get_evonodes_proposed_epoch_blocks_by_range_request::get_evonodes_proposed_epoch_blocks_by_range_request_v0::Start; use dapi_grpc::platform::v0::get_identities_contract_keys_request::GetIdentitiesContractKeysRequestV0; use dapi_grpc::platform::v0::get_path_elements_request::GetPathElementsRequestV0; @@ -2362,7 +2362,7 @@ impl FromProof for Propo Some(index) => try_u32_to_u16(index)?, None => try_u32_to_u16(mtd.epoch)?, }; - let checked_limit = limit.map(try_u32_to_u16).transpose()?; + let checked_limit = Some(proved_request_limit(limit)?); let (root_hash, proposer_block_counts) = Drive::verify_epoch_proposers( &proof.grovedb_proof, diff --git a/packages/rs-drive-proof-verifier/src/proof/groups.rs b/packages/rs-drive-proof-verifier/src/proof/groups.rs index 12d2144a876..24648d0ba49 100644 --- a/packages/rs-drive-proof-verifier/src/proof/groups.rs +++ b/packages/rs-drive-proof-verifier/src/proof/groups.rs @@ -1,7 +1,7 @@ use crate::error::MapGroveDbError; use crate::types::groups::{GroupActionSigners, GroupActions, Groups}; use crate::verify::verify_tenderdash_proof; -use crate::{ContextProvider, Error, FromProof}; +use crate::{proved_request_limit, ContextProvider, Error, FromProof}; use dapi_grpc::platform::v0::{ get_group_action_signers_request, get_group_actions_request, get_group_info_request, get_group_infos_request, GetGroupActionSignersRequest, GetGroupActionSignersResponse, @@ -109,7 +109,7 @@ impl FromProof for Groups { ) }); - let count = v0.count.map(|count| count as u16); + let count = Some(proved_request_limit(v0.count)?); (contract_id, start_group_contract_position, count) } @@ -195,7 +195,7 @@ impl FromProof for GroupActions { let group_contract_position = v0.group_contract_position as GroupContractPosition; - let count = v0.count.map(|count| count as u16); + let count = Some(proved_request_limit(v0.count)?); let status = GroupActionStatus::try_from(v0.status).map_err(|error| { Error::RequestError { diff --git a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs index 041de4ca662..1417d0b8b64 100644 --- a/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs +++ b/packages/rs-drive-proof-verifier/src/proof/token_pre_programmed_distributions.rs @@ -1,6 +1,6 @@ use crate::error::MapGroveDbError; use crate::verify::verify_tenderdash_proof; -use crate::{types::TokenPreProgrammedDistributions, ContextProvider, Error}; +use crate::{proved_request_limit, types::TokenPreProgrammedDistributions, ContextProvider, Error}; use dapi_grpc::platform::v0::{ get_token_pre_programmed_distributions_request, GetTokenPreProgrammedDistributionsRequest, GetTokenPreProgrammedDistributionsResponse, Proof, ResponseMetadata, @@ -68,14 +68,7 @@ impl FromProof for TokenPreProgrammed None => None, }; - let limit = req_v0 - .limit - .map(|l| { - u16::try_from(l).map_err(|_| Error::RequestError { - error: "limit exceeds u16::MAX".into(), - }) - }) - .transpose()?; + let limit = Some(proved_request_limit(req_v0.limit)?); let metadata = response .metadata() From 69856a7aac340a8f55ce44dc6f41041674b621a7 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 12:12:24 -0500 Subject: [PATCH 05/12] fix(proof-verifier): preserve omitted proved query limits --- .../src/from_request.rs | 42 +++++++++---------- .../rs-sdk/tests/fetch/contested_resource.rs | 18 +++++++- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/rs-drive-proof-verifier/src/from_request.rs b/packages/rs-drive-proof-verifier/src/from_request.rs index cc363a95db5..ecaf28572e7 100644 --- a/packages/rs-drive-proof-verifier/src/from_request.rs +++ b/packages/rs-drive-proof-verifier/src/from_request.rs @@ -27,7 +27,7 @@ use drive::query::{ VotePollsByEndDateDriveQuery, }; -use crate::{proved_request_limit, Error, DEFAULT_QUERY_LIMIT}; +use crate::{proved_request_limit, Error}; const BINCODE_CONFIG: dpp::bincode::config::Configuration = dpp::bincode::config::standard(); /// Convert a gRPC request into a query object. @@ -142,7 +142,7 @@ impl TryFromRequest for ContestedDocumentV Ok(proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 { prove:true, contract_id:self.vote_poll.contract_id.to_vec(), - count: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), + count: self.limit.map(|v| v as u32), document_type_name: self.vote_poll.document_type_name.clone(), index_name: self.vote_poll.index_name.clone(), index_values: self.vote_poll.index_values.iter().map(|v| @@ -211,7 +211,7 @@ impl TryFromRequest prove: true, identity_id: self.identity_id.to_vec(), offset: self.offset.map(|x| x as u32), - limit: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), + limit: self.limit.map(|x| x as u32), start_at_vote_poll_id_info: self.start_at.map(|(id, included)| { request_v0::StartAtVotePollIdInfo { start_at_poll_identifier: id.to_vec(), @@ -284,7 +284,7 @@ impl TryFromRequest dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e| Error::RequestError { error: e.to_string()})).collect::,_>>()?, order_ascending: self.order_ascending, - count: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), + count: self.limit.map(|v| v as u32), contestant_id: self.contestant_id.to_vec(), start_at_identifier_info: self.start_at.map(|v| request_v0::StartAtIdentifierInfo{ start_identifier: v.0.to_vec(), @@ -331,7 +331,7 @@ impl TryFromRequest for VotePollsByDocumentTypeQue Ok(GetContestedResourcesRequestV0 { prove: true, contract_id: self.contract_id.to_vec(), - count: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), + count: self.limit.map(|v| v as u32), document_type_name: self.document_type_name.clone(), end_index_values: bincode_encode_values(&self.end_index_values)?, start_index_values: bincode_encode_values(&self.start_index_values)?, @@ -399,7 +399,7 @@ impl TryFromRequest for VotePollsByEndDateDriveQue end_time_included, } }), - limit: Some(self.limit.unwrap_or(DEFAULT_QUERY_LIMIT) as u32), + limit: self.limit.map(|v| v as u32), offset: self.offset.map(|v| v as u32), ascending: self.order_ascending, } @@ -651,7 +651,7 @@ mod tests { } #[test] - fn test_contested_resources_request_defaults_limit_when_encoding() { + fn test_contested_resources_request_preserves_omitted_limit_when_encoding() { let query = VotePollsByDocumentTypeQuery { contract_id: Identifier::from_bytes(&[1u8; 32]).unwrap(), document_type_name: "domain".to_string(), @@ -667,7 +667,7 @@ mod tests { let proto::get_contested_resources_request::Version::V0(v0) = request.version.expect("request should contain a version"); - assert_eq!(v0.count, Some(DEFAULT_QUERY_LIMIT as u32)); + assert_eq!(v0.count, None); } #[test] @@ -689,11 +689,11 @@ mod tests { let query = VotePollsByDocumentTypeQuery::try_from_request(request).expect("request should decode"); - assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + assert_eq!(query.limit, Some(crate::DEFAULT_QUERY_LIMIT)); } #[test] - fn test_contested_resource_vote_state_request_defaults_limit_when_encoding() { + fn test_contested_resource_vote_state_request_preserves_omitted_limit_when_encoding() { let query = ContestedDocumentVotePollDriveQuery { vote_poll: sample_vote_poll(), result_type: ContestedDocumentVotePollDriveQueryResultType::Documents, @@ -707,7 +707,7 @@ mod tests { let proto::get_contested_resource_vote_state_request::Version::V0(v0) = request.version.expect("request should contain a version"); - assert_eq!(v0.count, Some(DEFAULT_QUERY_LIMIT as u32)); + assert_eq!(v0.count, None); } #[test] @@ -730,11 +730,11 @@ mod tests { let query = ContestedDocumentVotePollDriveQuery::try_from_request(request) .expect("request should decode"); - assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + assert_eq!(query.limit, Some(crate::DEFAULT_QUERY_LIMIT)); } #[test] - fn test_contested_resource_identity_votes_request_defaults_limit_when_encoding() { + fn test_contested_resource_identity_votes_request_preserves_omitted_limit_when_encoding() { let query = ContestedResourceVotesGivenByIdentityQuery { identity_id: Identifier::from_bytes(&[9u8; 32]).unwrap(), offset: None, @@ -747,7 +747,7 @@ mod tests { let proto::get_contested_resource_identity_votes_request::Version::V0(v0) = request.version.expect("request should contain a version"); - assert_eq!(v0.limit, Some(DEFAULT_QUERY_LIMIT as u32)); + assert_eq!(v0.limit, None); } #[test] @@ -766,11 +766,11 @@ mod tests { let query = ContestedResourceVotesGivenByIdentityQuery::try_from_request(request) .expect("request should decode"); - assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + assert_eq!(query.limit, Some(crate::DEFAULT_QUERY_LIMIT)); } #[test] - fn test_contested_resource_voters_for_identity_request_defaults_limit_when_encoding() { + fn test_contested_resource_voters_for_identity_request_preserves_omitted_limit_when_encoding() { let query = ContestedDocumentVotePollVotesDriveQuery { vote_poll: sample_vote_poll(), contestant_id: Identifier::from_bytes(&[3u8; 32]).unwrap(), @@ -784,7 +784,7 @@ mod tests { let proto::get_contested_resource_voters_for_identity_request::Version::V0(v0) = request.version.expect("request should contain a version"); - assert_eq!(v0.count, Some(DEFAULT_QUERY_LIMIT as u32)); + assert_eq!(v0.count, None); } #[test] @@ -807,11 +807,11 @@ mod tests { let query = ContestedDocumentVotePollVotesDriveQuery::try_from_request(request) .expect("request should decode"); - assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + assert_eq!(query.limit, Some(crate::DEFAULT_QUERY_LIMIT)); } #[test] - fn test_vote_polls_by_end_date_request_defaults_limit_when_encoding() { + fn test_vote_polls_by_end_date_request_preserves_omitted_limit_when_encoding() { let query = VotePollsByEndDateDriveQuery { start_time: Some((1000, true)), end_time: Some((2000, false)), @@ -824,7 +824,7 @@ mod tests { let proto::get_vote_polls_by_end_date_request::Version::V0(v0) = request.version.expect("request should contain a version"); - assert_eq!(v0.limit, Some(DEFAULT_QUERY_LIMIT as u32)); + assert_eq!(v0.limit, None); } #[test] @@ -843,7 +843,7 @@ mod tests { let query = VotePollsByEndDateDriveQuery::try_from_request(request).expect("request should decode"); - assert_eq!(query.limit, Some(DEFAULT_QUERY_LIMIT)); + assert_eq!(query.limit, Some(crate::DEFAULT_QUERY_LIMIT)); } // --------------------------------------------------------------- diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index aefcc309c81..6174bb856f2 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -4,7 +4,12 @@ use crate::fetch::{ common::{setup_logs, setup_sdk_for_test_case, TEST_DPNS_NAME}, config::Config, }; -use dash_sdk::{platform::FetchMany, Error}; +use dapi_grpc::platform::v0::get_contested_resources_request; +use dapi_grpc::platform::v0::GetContestedResourcesRequest; +use dash_sdk::{ + platform::{FetchMany, Query}, + Error, +}; use dpp::{ platform_value::Value, voting::{ @@ -483,6 +488,17 @@ async fn contested_resources_start_index_values_no_limit_proof_failure() { order_ascending: true, }; + let request: GetContestedResourcesRequest = make_query(None) + .clone() + .query(true) + .expect("query should serialize"); + let get_contested_resources_request::Version::V0(v0) = + request.version.expect("request should contain version"); + assert_eq!( + v0.count, None, + "omitted query limit must stay omitted on the wire", + ); + // 1. With limit → works fine. let with_limit = ContestedResource::fetch_many(&sdk, make_query(Some(100))) .await From ee74646af57192faeee707edcf342e3b4aa2e7dd Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 12:24:13 -0500 Subject: [PATCH 06/12] refactor(proof-verifier): share drive default query limit --- packages/rs-drive-proof-verifier/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-drive-proof-verifier/src/lib.rs b/packages/rs-drive-proof-verifier/src/lib.rs index 5f4375fc0eb..ab8b95fe381 100644 --- a/packages/rs-drive-proof-verifier/src/lib.rs +++ b/packages/rs-drive-proof-verifier/src/lib.rs @@ -25,7 +25,7 @@ pub mod unproved; /// /// Proof verification must mirror this behavior; otherwise a valid proof for a default-bounded /// server query is reconstructed locally as an unbounded query and looks truncated. -pub(crate) const DEFAULT_QUERY_LIMIT: u16 = 100; +pub(crate) use drive::config::DEFAULT_QUERY_LIMIT; /// Parse a proved request's optional `count`/`limit`, applying Platform's default when omitted. pub(crate) fn proved_request_limit(limit: Option) -> Result { From ed6db4e7bebcd23fb5a341e4ca645e61a73b6778 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 12:28:20 -0500 Subject: [PATCH 07/12] test(sdk): require omitted contested proof to verify --- .../rs-sdk/tests/fetch/contested_resource.rs | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 6174bb856f2..2ea0597e865 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -467,9 +467,9 @@ pub async fn check_mn_voting_prerequisites(cfg: &Config) -> Result<(), Vec { - let msg = e.to_string(); - tracing::error!(%msg, "Confirmed: no-limit proof verification failure"); - assert!( - msg.contains("Proof is missing data for query range") - || msg.contains("invalid proof"), - "Expected proof verification error, got: {}", - msg - ); - } - Ok(ref resources) => { - // If this branch is reached, the platform bug has been fixed. - // Update this test to always expect Ok once the fix lands. - tracing::info!( - count = resources.0.len(), - "No-limit query now succeeds — bug is fixed!" - ); - assert!( - !resources.0.is_empty(), - "expected contested labels under 'dash'" - ); - } - } + // 2. Without limit → should now verify successfully and match the explicit default limit. + let no_limit = ContestedResource::fetch_many(&sdk, make_query(None)) + .await + .expect("query with omitted limit should verify successfully"); + + tracing::info!(count = no_limit.0.len(), "No-limit query: OK"); + assert!(!no_limit.0.is_empty(), "expected contested labels under 'dash'"); + assert_eq!( + no_limit.0, with_limit.0, + "omitted-limit proof should match the explicit default-limit result set", + ); } From e08f71daa979d0faab8ff5ce812211122602e04d Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 13:02:09 -0500 Subject: [PATCH 08/12] fix(platform): address verified review feedback --- .../rs-sdk/tests/fetch/contested_resource.rs | 19 +++-- .../repro-default-limit-proof-mismatches.mjs | 79 ++++++++++++++----- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 2ea0597e865..1c84127155e 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -442,8 +442,8 @@ pub async fn check_mn_voting_prerequisites(cfg: &Config) -> Result<(), Vec Result<(), Vec Result<(), Vec Date: Tue, 21 Apr 2026 13:03:05 -0500 Subject: [PATCH 09/12] chore(platform): drop repro script from repo --- .../repro-default-limit-proof-mismatches.mjs | 784 ------------------ 1 file changed, 784 deletions(-) delete mode 100644 scripts/repro-default-limit-proof-mismatches.mjs diff --git a/scripts/repro-default-limit-proof-mismatches.mjs b/scripts/repro-default-limit-proof-mismatches.mjs deleted file mode 100644 index fb308764afe..00000000000 --- a/scripts/repro-default-limit-proof-mismatches.mjs +++ /dev/null @@ -1,784 +0,0 @@ -#!/usr/bin/env node - -import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; - -/* - * Reproduce proof-verification mismatches caused by omitted limits. - * - * The bug shape is: - * - Platform applies a default limit when count/limit is omitted - * - The proof verifier rebuilds the request as if no limit was applied - * - The same query succeeds when an explicit limit is supplied - * - * Usage: - * node scripts/repro-default-limit-proof-mismatches.mjs - * - * If you are running from this monorepo with Yarn Plug'n'Play, use the - * package-aware runtime instead of plain Node. For example: - * yarn node scripts/repro-default-limit-proof-mismatches.mjs - * - * Optional environment variables: - * EVO_SDK_IMPORT - * Alternate module specifier for EvoSDK. If omitted, the script tries: - * 1. "@dashevo/evo-sdk" - * 2. the local workspace build at packages/js-evo-sdk/dist/sdk.js - * - * NETWORK - * mainnet | testnet | local. Defaults to mainnet. - * - * TRUSTED - * true | false. Defaults to true. - * - * DEFAULT_LIMIT - * Explicit limit to use for the control query. Defaults to 100. - * - * DPNS_CONTRACT_ID - * Defaults to the DPNS mainnet contract used in the earlier repro. - * - * DPNS_DOCUMENT_TYPE - * Defaults to "domain". - * - * DPNS_INDEX - * Defaults to "parentNameAndLabel". - * - * CONTESTED_PARENT - * Defaults to "dash". - * - * VOTE_STATE_LABEL - * Optional explicit label for contestedResourceVoteState repro. - * - * VOTERS_LABEL - * VOTERS_CONTESTANT_ID - * Optional explicit fixture for contestedResourceVotersForIdentity repro. - * - * IDENTITY_VOTES_IDENTITY_ID - * Optional explicit fixture for contestedResourceIdentityVotes repro. - * - * VOTE_POLLS_START_MS - * VOTE_POLLS_END_MS - * Optional explicit time range for votePollsByEndDate repro. - * - * GROUP_INFOS_CONTRACT_ID - * Optional fixture for group.infos repro. - * - * GROUP_ACTIONS_CONTRACT_ID - * GROUP_ACTIONS_POSITION - * GROUP_ACTIONS_STATUS - * Optional fixture for group.actions repro. - * - * TOKEN_DISTRIBUTIONS_TOKEN_ID - * Reserved for a future EvoSDK token pre-programmed distributions repro. - * The current JS EvoSDK does not expose this query yet, so the script - * reports it as skipped. - */ - -const DEFAULT_DPNS_CONTRACT_ID = - process.env.DPNS_CONTRACT_ID ?? 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'; -const DEFAULT_DPNS_DOCUMENT_TYPE = process.env.DPNS_DOCUMENT_TYPE ?? 'domain'; -const DEFAULT_DPNS_INDEX = process.env.DPNS_INDEX ?? 'parentNameAndLabel'; -const DEFAULT_PARENT = process.env.CONTESTED_PARENT ?? 'dash'; -const NETWORK = process.env.NETWORK ?? 'mainnet'; -const TRUSTED = (process.env.TRUSTED ?? 'true').toLowerCase() !== 'false'; -const localSdkImport = new URL('../packages/js-evo-sdk/dist/sdk.js', import.meta.url).href; -const EVO_SDK_IMPORT = process.env.EVO_SDK_IMPORT ?? null; - -const nowMs = Date.now(); - -function printHeading(title) { - console.log(`\n=== ${title} ===`); -} - -function parseNumericEnv(name, defaultValue, { integer = false } = {}) { - const raw = process.env[name]; - - if (raw == null || raw === '') { - return defaultValue; - } - - const parsed = Number(raw); - if (!Number.isFinite(parsed) || (integer && !Number.isInteger(parsed))) { - throw new Error( - `Invalid ${name}: expected ${integer ? 'an integer' : 'a finite number'}, got ${JSON.stringify(raw)}`, - ); - } - - return parsed; -} - -const DEFAULT_LIMIT = parseNumericEnv('DEFAULT_LIMIT', 100, { integer: true }); -const defaultVotePollsStartMs = parseNumericEnv('VOTE_POLLS_START_MS', 0, { integer: true }); -const defaultVotePollsEndMs = parseNumericEnv( - 'VOTE_POLLS_END_MS', - nowMs + 365 * 24 * 60 * 60 * 1000, - { integer: true }, -); - -function shorten(value, max = 140) { - const text = typeof value === 'string' ? value : JSON.stringify(value); - if (text.length <= max) { - return text; - } - return `${text.slice(0, max - 3)}...`; -} - -function normalizeError(error) { - if (!error) { - return 'Unknown error'; - } - return error?.message ?? String(error); -} - -function countResult(result) { - if (Array.isArray(result)) { - return result.length; - } - if (result instanceof Map) { - return result.size; - } - if (typeof result?.size === 'number') { - return result.size; - } - if (typeof result?.length === 'number') { - return result.length; - } - return null; -} - -function summarizeResult(result) { - if (Array.isArray(result)) { - return shorten(result.slice(0, 5).map(stringifySdkValue)); - } - if (result instanceof Map) { - return shorten( - Array.from(result.entries()) - .slice(0, 5) - .map(([key, value]) => [key, stringifySdkValue(value)]), - ); - } - return shorten(stringifySdkValue(result)); -} - -function stringifySdkValue(value) { - if (value == null) { - return value; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (Array.isArray(value)) { - return value.map(stringifySdkValue); - } - if (value instanceof Uint8Array) { - return Array.from(value); - } - if (typeof value?.toJSON === 'function') { - try { - return value.toJSON(); - } catch { - // ignore - } - } - if (typeof value?.toString === 'function' && value.toString !== Object.prototype.toString) { - try { - const text = value.toString(); - if (text && text !== '[object Object]') { - return text; - } - } catch { - // ignore - } - } - return value; -} - -function identifierToString(id) { - if (!id) { - return null; - } - if (typeof id === 'string') { - return id; - } - if (typeof id.toString === 'function') { - return id.toString(); - } - return String(id); -} - -async function buildSdk(EvoSDK) { - const builderName = `${NETWORK}${TRUSTED ? 'Trusted' : ''}`; - if (typeof EvoSDK[builderName] !== 'function') { - throw new Error(`Unsupported NETWORK/TRUSTED combination: ${builderName}`); - } - - const sdk = await EvoSDK[builderName](); - await sdk.connect(); - return sdk; -} - -function resolveBareImportFromNodeModules(specifier) { - let currentDir = process.cwd(); - - while (true) { - const packageDir = path.join(currentDir, 'node_modules', specifier); - const packageJsonPath = path.join(packageDir, 'package.json'); - - if (existsSync(packageJsonPath)) { - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - const exportEntry = packageJson.exports?.['.']; - const relativeEntry = - (typeof exportEntry === 'string' && exportEntry) - || exportEntry?.import - || packageJson.module - || packageJson.main; - - if (!relativeEntry) { - throw new Error(`Could not determine entry point from ${packageJsonPath}`); - } - - return pathToFileURL(path.join(packageDir, relativeEntry)).href; - } - - const parentDir = path.dirname(currentDir); - if (parentDir === currentDir) { - break; - } - currentDir = parentDir; - } - - return null; -} - -async function loadEvoSdk() { - const candidates = EVO_SDK_IMPORT - ? [EVO_SDK_IMPORT] - : ['@dashevo/evo-sdk', localSdkImport]; - - const failures = []; - for (const specifier of candidates) { - try { - let target = specifier; - if ( - !specifier.startsWith('.') - && !specifier.startsWith('/') - && !specifier.startsWith('file:') - ) { - target = resolveBareImportFromNodeModules(specifier) ?? specifier; - } - - const mod = await import(target); - if (mod?.EvoSDK) { - return mod.EvoSDK; - } - failures.push(`${specifier}: imported, but no EvoSDK export was found`); - } catch (error) { - failures.push(`${specifier}: ${normalizeError(error)}`); - } - } - - throw new Error( - [ - 'Unable to import EvoSDK.', - ...failures.map((failure) => `- ${failure}`), - "If you are in this monorepo, run via `yarn node` so Plug'n'Play dependencies resolve.", - 'If you want the published package, install `@dashevo/evo-sdk` and optionally set EVO_SDK_IMPORT=@dashevo/evo-sdk.', - ].join('\n'), - ); -} - -async function runPair(name, buildNoLimit, buildWithLimit) { - const record = { - name, - noLimit: null, - explicitLimit: null, - conclusion: null, - }; - - const cases = [ - ['noLimit', buildNoLimit], - ['explicitLimit', buildWithLimit], - ]; - - for (const [key, build] of cases) { - let result; - - try { - result = await build(); - } catch (queryError) { - record[key] = { - ok: false, - error: normalizeError(queryError), - }; - continue; - } - - const value = { ok: true }; - - try { - value.count = countResult(result); - } catch (formatError) { - value.count = null; - value.formattingError = normalizeError(formatError); - } - - try { - value.summary = summarizeResult(result); - } catch (formatError) { - value.summary = ''; - const normalized = normalizeError(formatError); - value.formattingError = value.formattingError - ? `${value.formattingError}; ${normalized}` - : normalized; - } - - record[key] = value; - } - - if (record.noLimit?.ok === false && record.explicitLimit?.ok === true) { - record.conclusion = 'CONFIRMED_DEFAULT_LIMIT_MISMATCH'; - } else if (record.noLimit?.ok === true && record.explicitLimit?.ok === true) { - record.conclusion = 'NO_FAILURE_WITH_CURRENT_FIXTURE'; - } else if (record.noLimit?.ok === false && record.explicitLimit?.ok === false) { - record.conclusion = 'FIXTURE_OR_ENDPOINT_FAILED_BOTH_WAYS'; - } else { - record.conclusion = 'INCONCLUSIVE'; - } - - return record; -} - -function printRecord(record) { - console.log(`\n[${record.conclusion}] ${record.name}`); - - for (const key of ['noLimit', 'explicitLimit']) { - const value = record[key]; - if (!value) { - console.log(` ${key}: skipped`); - continue; - } - - if (value.ok) { - const count = value.count == null ? 'n/a' : value.count; - console.log(` ${key}: ok, count=${count}, sample=${value.summary}`); - } else { - console.log(` ${key}: error=${value.error}`); - } - } -} - -function getTokenPreProgrammedDistributionsMethod(sdk) { - const candidateNames = [ - 'preProgrammedDistributions', - 'tokenPreProgrammedDistributions', - 'getPreProgrammedDistributions', - ]; - - for (const name of candidateNames) { - if (typeof sdk?.tokens?.[name] === 'function') { - return sdk.tokens[name].bind(sdk.tokens); - } - } - - return null; -} - -async function autoDiscoverContestedLabel(sdk) { - const explicit = process.env.VOTE_STATE_LABEL; - if (explicit) { - return { label: explicit, labels: undefined }; - } - - const labels = await sdk.group.contestedResources({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - startIndexValues: [DEFAULT_PARENT], - limit: DEFAULT_LIMIT, - orderAscending: true, - }); - - const activeEntries = await sdk.voting.votePollsByEndDate({ - startTimeMs: defaultVotePollsStartMs, - startTimeIncluded: true, - endTimeMs: defaultVotePollsEndMs, - endTimeIncluded: true, - orderAscending: true, - limit: DEFAULT_LIMIT, - }); - - for (const entry of activeEntries) { - for (const poll of entry.votePolls) { - const json = poll.toJSON?.(); - const values = json?.contestedDocumentResourceVotePoll?.indexValues; - if (Array.isArray(values) && values[0] === DEFAULT_PARENT && typeof values[1] === 'string') { - return { label: values[1], labels }; - } - } - } - - return { label: labels[0] ?? null, labels }; -} - -async function autoDiscoverContestantId(sdk, label) { - if (process.env.VOTERS_CONTESTANT_ID) { - return process.env.VOTERS_CONTESTANT_ID; - } - - if (!label) { - return null; - } - - const state = await sdk.voting.contestedResourceVoteState({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - indexValues: [DEFAULT_PARENT, label], - resultType: 'documentsAndVoteTally', - includeLockedAndAbstaining: true, - limit: DEFAULT_LIMIT, - }); - - const contender = state?.contenders?.[0]; - return identifierToString(contender?.identityId); -} - -async function autoDiscoverIdentityVotesIdentityId(sdk, label, contestantId) { - if (process.env.IDENTITY_VOTES_IDENTITY_ID) { - return process.env.IDENTITY_VOTES_IDENTITY_ID; - } - - if (!label || !contestantId) { - return null; - } - - try { - const voters = await sdk.group.contestedResourceVotersForIdentity({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - indexValues: [DEFAULT_PARENT, label], - contestantId, - orderAscending: true, - limit: DEFAULT_LIMIT, - }); - - return identifierToString(voters?.[0]); - } catch { - return null; - } -} - -async function main() { - const EvoSDK = await loadEvoSdk(); - const sdk = await buildSdk(EvoSDK); - - try { - printHeading(`Connected to ${NETWORK} (${TRUSTED ? 'trusted' : 'untrusted'})`); - console.log(`using explicit control limit = ${DEFAULT_LIMIT}`); - - const records = []; - - records.push( - await runPair( - 'group.contestedResources(startIndexValues=["dash"])', - () => - sdk.group.contestedResources({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - startIndexValues: [DEFAULT_PARENT], - orderAscending: true, - }), - () => - sdk.group.contestedResources({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - startIndexValues: [DEFAULT_PARENT], - orderAscending: true, - limit: DEFAULT_LIMIT, - }), - ), - ); - - const currentEpoch = await sdk.epoch.current(); - const epochIndex = Number(currentEpoch.index ?? currentEpoch); - records.push( - await runPair( - `epoch.evonodesProposedBlocksByRange(epoch=${epochIndex})`, - () => - sdk.epoch.evonodesProposedBlocksByRange({ - epoch: epochIndex, - orderAscending: true, - }), - () => - sdk.epoch.evonodesProposedBlocksByRange({ - epoch: epochIndex, - orderAscending: true, - limit: DEFAULT_LIMIT, - }), - ), - ); - - const { label } = await autoDiscoverContestedLabel(sdk); - if (label) { - records.push( - await runPair( - `voting.contestedResourceVoteState(label=${label})`, - () => - sdk.voting.contestedResourceVoteState({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - indexValues: [DEFAULT_PARENT, label], - resultType: 'documentsAndVoteTally', - includeLockedAndAbstaining: true, - }), - () => - sdk.voting.contestedResourceVoteState({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - indexValues: [DEFAULT_PARENT, label], - resultType: 'documentsAndVoteTally', - includeLockedAndAbstaining: true, - limit: DEFAULT_LIMIT, - }), - ), - ); - - const contestantId = await autoDiscoverContestantId(sdk, process.env.VOTERS_LABEL ?? label); - if (contestantId) { - const votersLabel = process.env.VOTERS_LABEL ?? label; - records.push( - await runPair( - `group.contestedResourceVotersForIdentity(label=${votersLabel}, contestantId=${contestantId})`, - () => - sdk.group.contestedResourceVotersForIdentity({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - indexValues: [DEFAULT_PARENT, votersLabel], - contestantId, - orderAscending: true, - }), - () => - sdk.group.contestedResourceVotersForIdentity({ - dataContractId: DEFAULT_DPNS_CONTRACT_ID, - documentTypeName: DEFAULT_DPNS_DOCUMENT_TYPE, - indexName: DEFAULT_DPNS_INDEX, - indexValues: [DEFAULT_PARENT, votersLabel], - contestantId, - orderAscending: true, - limit: DEFAULT_LIMIT, - }), - ), - ); - - const identityVotesIdentityId = await autoDiscoverIdentityVotesIdentityId( - sdk, - votersLabel, - contestantId, - ); - - if (identityVotesIdentityId) { - records.push( - await runPair( - `voting.contestedResourceIdentityVotes(identityId=${identityVotesIdentityId})`, - () => - sdk.voting.contestedResourceIdentityVotes({ - identityId: identityVotesIdentityId, - orderAscending: true, - }), - () => - sdk.voting.contestedResourceIdentityVotes({ - identityId: identityVotesIdentityId, - orderAscending: true, - limit: DEFAULT_LIMIT, - }), - ), - ); - } else { - records.push({ - name: 'voting.contestedResourceIdentityVotes', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_IDENTITY_FIXTURE', - }); - } - } else { - records.push({ - name: 'group.contestedResourceVotersForIdentity', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_CONTESTANT_FIXTURE', - }); - records.push({ - name: 'voting.contestedResourceIdentityVotes', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_IDENTITY_FIXTURE', - }); - } - } else { - records.push({ - name: 'voting.contestedResourceVoteState', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_LABEL_DISCOVERED', - }); - records.push({ - name: 'group.contestedResourceVotersForIdentity', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_CONTESTANT_FIXTURE', - }); - records.push({ - name: 'voting.contestedResourceIdentityVotes', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_IDENTITY_FIXTURE', - }); - } - - records.push( - await runPair( - `voting.votePollsByEndDate(start=${defaultVotePollsStartMs}, end=${defaultVotePollsEndMs})`, - () => - sdk.voting.votePollsByEndDate({ - startTimeMs: defaultVotePollsStartMs, - startTimeIncluded: true, - endTimeMs: defaultVotePollsEndMs, - endTimeIncluded: true, - orderAscending: true, - }), - () => - sdk.voting.votePollsByEndDate({ - startTimeMs: defaultVotePollsStartMs, - startTimeIncluded: true, - endTimeMs: defaultVotePollsEndMs, - endTimeIncluded: true, - orderAscending: true, - limit: DEFAULT_LIMIT, - }), - ), - ); - - if (process.env.GROUP_INFOS_CONTRACT_ID) { - records.push( - await runPair( - `group.infos(contractId=${process.env.GROUP_INFOS_CONTRACT_ID})`, - () => - sdk.group.infos({ - dataContractId: process.env.GROUP_INFOS_CONTRACT_ID, - }), - () => - sdk.group.infos({ - dataContractId: process.env.GROUP_INFOS_CONTRACT_ID, - limit: DEFAULT_LIMIT, - }), - ), - ); - } else { - records.push({ - name: 'group.infos', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_GROUP_INFOS_CONTRACT_ID', - }); - } - - if ( - process.env.GROUP_ACTIONS_CONTRACT_ID - && process.env.GROUP_ACTIONS_POSITION - && process.env.GROUP_ACTIONS_STATUS - ) { - const groupContractPosition = parseNumericEnv('GROUP_ACTIONS_POSITION', null, { - integer: true, - }); - records.push( - await runPair( - `group.actions(contractId=${process.env.GROUP_ACTIONS_CONTRACT_ID}, position=${groupContractPosition})`, - () => - sdk.group.actions({ - dataContractId: process.env.GROUP_ACTIONS_CONTRACT_ID, - groupContractPosition, - status: process.env.GROUP_ACTIONS_STATUS, - }), - () => - sdk.group.actions({ - dataContractId: process.env.GROUP_ACTIONS_CONTRACT_ID, - groupContractPosition, - status: process.env.GROUP_ACTIONS_STATUS, - limit: DEFAULT_LIMIT, - }), - ), - ); - } else { - records.push({ - name: 'group.actions', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_GROUP_ACTIONS_FIXTURE', - }); - } - - const tokenDistributionsMethod = getTokenPreProgrammedDistributionsMethod(sdk); - if (!tokenDistributionsMethod) { - records.push({ - name: 'tokens.preProgrammedDistributions', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NOT_EXPOSED_BY_EVO_SDK', - }); - } else if (!process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID) { - records.push({ - name: 'tokens.preProgrammedDistributions', - noLimit: null, - explicitLimit: null, - conclusion: 'SKIPPED_NO_TOKEN_DISTRIBUTIONS_TOKEN_ID', - }); - } else { - records.push( - await runPair( - `tokens.preProgrammedDistributions(tokenId=${process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID})`, - () => - tokenDistributionsMethod({ - tokenId: process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID, - }), - () => - tokenDistributionsMethod({ - tokenId: process.env.TOKEN_DISTRIBUTIONS_TOKEN_ID, - limit: DEFAULT_LIMIT, - }), - ), - ); - } - - printHeading('Results'); - for (const record of records) { - printRecord(record); - } - - const confirmed = records.filter( - (record) => record.conclusion === 'CONFIRMED_DEFAULT_LIMIT_MISMATCH', - ); - printHeading('Summary'); - console.log(`confirmed mismatches: ${confirmed.length}`); - for (const record of confirmed) { - console.log(`- ${record.name}`); - } - } finally { - try { - await sdk.disconnect?.(); - } catch { - // ignore disconnect failures - } - } -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); From 09a7bdda93f57f96f44a22614e7e3925c384f0c3 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 13:11:04 -0500 Subject: [PATCH 10/12] style(rs-sdk): format contested resource test --- packages/rs-sdk/tests/fetch/contested_resource.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 1c84127155e..4497ac7957f 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -500,12 +500,10 @@ async fn should_contested_resources_start_index_values_no_limit_match_explicit_d ); // 1. With limit → works fine. - let with_limit = ContestedResource::fetch_many( - &sdk, - make_query(Some(drive::config::DEFAULT_QUERY_LIMIT)), - ) - .await - .expect("query with start_index_values + limit should succeed"); + let with_limit = + ContestedResource::fetch_many(&sdk, make_query(Some(drive::config::DEFAULT_QUERY_LIMIT))) + .await + .expect("query with start_index_values + limit should succeed"); tracing::info!(count = with_limit.0.len(), "With limit: OK"); assert!(!with_limit.0.is_empty(), "expected contested labels"); @@ -515,7 +513,10 @@ async fn should_contested_resources_start_index_values_no_limit_match_explicit_d .expect("query with omitted limit should verify successfully"); tracing::info!(count = no_limit.0.len(), "No-limit query: OK"); - assert!(!no_limit.0.is_empty(), "expected contested labels under 'dash'"); + assert!( + !no_limit.0.is_empty(), + "expected contested labels under 'dash'" + ); assert_eq!( no_limit.0, with_limit.0, "omitted-limit proof should match the explicit default-limit result set", From a4fd818bbd28e9528d5b8221c4456c36540dc244 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 21 Apr 2026 13:36:56 -0500 Subject: [PATCH 11/12] test(rs-sdk): add contested resource proof vectors --- .../rs-sdk/tests/fetch/contested_resource.rs | 4 ++-- .../.gitkeep | 0 ...7b5b7e0a1d712a09c40d5721f622bf53c53155.json | 1 + ...d1cf33dd52735f597de4b4c804effd2600d135.json | Bin 0 -> 622051 bytes ...5799af81392b7b0cbb7e86412da37ab13aef4b.json | Bin 0 -> 622052 bytes ...82d40e0804fdbde0e6115133f9a9348267cace.json | 1 + 6 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/.gitkeep create mode 100644 packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/data_contract-e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155.json create mode 100644 packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/msg_GetContestedResourcesRequest_8f4daadf3e41747492cd381cbbd1cf33dd52735f597de4b4c804effd2600d135.json create mode 100644 packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/msg_GetContestedResourcesRequest_8f71462d5f438e1b12fedf94ad5799af81392b7b0cbb7e86412da37ab13aef4b.json create mode 100644 packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/quorum_pubkey-6-000000928ce4e3adf20561462b82d40e0804fdbde0e6115133f9a9348267cace.json diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 4497ac7957f..946059ba4d0 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -466,8 +466,8 @@ pub async fn check_mn_voting_prerequisites(cfg: &Config) -> Result<(), Vecwu(0TiRzi50VSm*f11Z35?j1A_J8>5yYKn8 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 literal 0 HcmV?d00001 diff --git a/packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/msg_GetContestedResourcesRequest_8f71462d5f438e1b12fedf94ad5799af81392b7b0cbb7e86412da37ab13aef4b.json b/packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/msg_GetContestedResourcesRequest_8f71462d5f438e1b12fedf94ad5799af81392b7b0cbb7e86412da37ab13aef4b.json new file mode 100644 index 0000000000000000000000000000000000000000..d52a58f76f78693666417b515bb793b8a68e7343 GIT binary patch literal 622052 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 literal 0 HcmV?d00001 diff --git a/packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/quorum_pubkey-6-000000928ce4e3adf20561462b82d40e0804fdbde0e6115133f9a9348267cace.json b/packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/quorum_pubkey-6-000000928ce4e3adf20561462b82d40e0804fdbde0e6115133f9a9348267cace.json new file mode 100644 index 00000000000..41ce883b732 --- /dev/null +++ b/packages/rs-sdk/tests/vectors/contested_resources_start_index_values_proof_bug/quorum_pubkey-6-000000928ce4e3adf20561462b82d40e0804fdbde0e6115133f9a9348267cace.json @@ -0,0 +1 @@ +b1f60096ca81649bb3ed1f2c326e9b6a602dd615ce9e116509e6f0307261601a7ecc524366368b98030cd2cd45c1c7d2 \ No newline at end of file From c05b9699ac30376090684d3848903891b0a9f6e5 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 29 Apr 2026 23:14:02 +0700 Subject: [PATCH 12/12] test(drive): regression for document proof default-limit mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstrates that DriveDocumentQuery has the same proof-mismatch bug this PR fixes for contested resources, but on the documents path: TryFrom<&DocumentQuery> for DriveDocumentQuery maps wire `limit==0` to `limit: None` while the server applies `default_query_limit`. The asymmetric SizedQuery trips GroveDB's verify_query with the same "Proof is missing data for query range" failure. Test inserts >DEFAULT_QUERY_LIMIT documents, proves with `limit=Some(100)`, verifies with `limit=None`, expects success — fails today, will pass once `proved_request_limit` (or an equivalent SDK-side fix) is extended to the documents path. Left intentionally non-ignored so CI surfaces the gap; resolve before merge or track as an explicit follow-up. --- .../verify_proof_keep_serialized/v0/mod.rs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/v0/mod.rs b/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/v0/mod.rs index 97984c1ac0f..9d355b7c60f 100644 --- a/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/v0/mod.rs +++ b/packages/rs-drive/src/verify/document/verify_proof_keep_serialized/v0/mod.rs @@ -142,6 +142,105 @@ mod tests { } } + /// Regression test exposing the same proof-mismatch class of bug this PR + /// fixes for contested resources, but on the documents path — which this + /// PR does NOT address. + /// + /// Server (`packages/rs-drive-abci/src/query/document_query/v0/mod.rs:124`) + /// applies `default_query_limit` when wire `limit == 0`, while the SDK's + /// `TryFrom<&DocumentQuery> for DriveDocumentQuery` + /// (`packages/rs-sdk/src/platform/documents/document_query.rs:325-329`) + /// maps wire `limit == 0` back to `limit: None`. The asymmetric + /// `SizedQuery` flowing through `DriveDocumentQuery::construct_path_query` + /// (`packages/rs-drive/src/query/mod.rs:1288, 1328, 2113`) then trips + /// GroveDB's `verify_query` with the same "Proof is missing data for + /// query range" failure that motivated this PR. + /// + /// `proved_request_limit` introduced in this PR is not applied to the + /// document path; that gap should be closed before merge or in a tracked + /// follow-up. Until then this test fails CI to keep the regression + /// visible. + #[test] + fn document_proof_with_limit_some_prove_vs_limit_none_verify() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let contract = load_system_data_contract(SystemDataContract::DPNS, platform_version) + .expect("expected to load DPNS contract"); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + None, + None, + platform_version, + ) + .expect("expected to apply contract"); + + let document_type = contract + .document_type_for_name("preorder") + .expect("expected preorder document type"); + + // Insert > DEFAULT_QUERY_LIMIT documents so the bounded vs unbounded + // queries cannot trivially produce identical proofs. + let document_count = 110u64; + for seed in 1..=document_count { + let document = document_type + .random_document(Some(seed), platform_version) + .expect("expected a random document"); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("expected to insert document"); + } + + // Prover applies `default_query_limit` (server side). + let prover_query = DriveDocumentQuery::all_items_query( + &contract, + document_type, + Some(crate::config::DEFAULT_QUERY_LIMIT), + ); + + let (proof, _cost) = prover_query + .execute_with_proof(&drive, None, None, platform_version) + .expect("expected to execute query with proof"); + + // Verifier reconstructs with `limit: None`, mirroring what the SDK + // does today for omitted document-query limits. + let verifier_query = DriveDocumentQuery::all_items_query(&contract, document_type, None); + + let (_root, docs) = verifier_query + .verify_proof_keep_serialized(proof.as_slice(), platform_version) + .expect( + "verify with limit=None should succeed against a limit=Some(DEFAULT_QUERY_LIMIT) \ + proof; failure here indicates the document path needs the same \ + `proved_request_limit` treatment the contested-resource paths receive in this PR", + ); + + assert_eq!( + docs.len(), + crate::config::DEFAULT_QUERY_LIMIT as usize, + "expected verifier to return DEFAULT_QUERY_LIMIT documents when prover bounded the result set" + ); + } + #[test] fn should_prove_and_verify_keep_serialized_empty_result() { let drive = setup_drive_with_initial_state_structure(None);