From 4347432ac636249627e27f1e754efa4a1dfcc83f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 00:31:33 +0700 Subject: [PATCH 01/10] feat(platform)!: prove data contract versions without the contracts via a PV14 version item `getDataContractsLatestVersions` (#4739) exists so a client can check that the contracts it holds are still current, but its proved form was the multi-contract proof: GroveDB proves a matched key together with its value, and the version lived only inside the serialized contract, so the proof carried every contract whatever `include_contracts` said. From protocol version 14 every contract now carries a four-byte big-endian version item at key 2 of its root subtree (`[64, id] / 2`), beside the contract (key 0) and its documents (key 1): - `add_contract_to_storage` v1, selected by the new `DRIVE_CONTRACT_METHOD_VERSIONS_V4` that `DRIVE_VERSION_V9` (PV14) uses, writes the item on every contract create and update, for both the plain and the history-keeping layout, with the contract element's flags. - The first block of PV14 backfills the item for every contract already in state (`Drive::add_version_items_to_all_contracts`, idempotent, paged). - A new query helper slot, `data_contract_query_helpers.latest_versions_read` (0 in the PV1-13 tables, 1 in `DRIVE_ABCI_QUERY_VERSIONS_V3`), gates the query without a wire change. Without `include_contracts` the proved form returns `prove_contracts_versions`, a proof of the items verified by `Drive::verify_contracts_versions`, and the unproved form answers from the contract cache on a hit and from the item (`fetch_contract_version`) on a miss, never loading a contract. With `include_contracts`, or on a state without the items, both forms behave as before. - The proof verifier branches on the same slot, so the Rust SDK, wasm-sdk and Evo SDK verify the small proof once they observe protocol version 14. The PV14 pinned app hash in the deterministic root hash test moves because of the extra element under the contract subtree. Devnets already running PV14 dev builds need a reset, since the first-block hook does not fire for them. Co-Authored-By: Claude Fable 5.1 --- book/src/drive/indexes.md | 2 +- .../protos/platform/v0/platform.proto | 22 +- .../v0/mod.rs | 68 ++++ .../data_contracts_latest_versions/v0/mod.rs | 329 ++++++++++++++++-- .../proof/data_contracts_latest_versions.rs | 92 +++-- .../types/data_contracts_latest_versions.rs | 4 +- .../get_fetch/fetch_contract_version/mod.rs | 52 +++ .../fetch_contract_version/v0/mod.rs | 70 ++++ .../src/drive/contract/get_fetch/mod.rs | 1 + .../insert/add_contract_to_storage/mod.rs | 13 +- .../insert/add_contract_to_storage/v1/mod.rs | 225 ++++++++++++ .../add_version_items_to_all_contracts.rs | 245 +++++++++++++ .../src/drive/contract/migration/mod.rs | 1 + packages/rs-drive/src/drive/contract/mod.rs | 2 + packages/rs-drive/src/drive/contract/paths.rs | 6 + .../rs-drive/src/drive/contract/prove/mod.rs | 3 + .../prove/prove_contracts_versions/mod.rs | 56 +++ .../prove/prove_contracts_versions/v0/mod.rs | 31 ++ .../rs-drive/src/drive/contract/queries.rs | 27 +- .../src/drive/contract/version_item.rs | 45 +++ packages/rs-drive/src/verify/contract/mod.rs | 1 + .../contract/verify_contracts_versions/mod.rs | 57 +++ .../verify_contracts_versions/v0/mod.rs | 310 +++++++++++++++++ .../rs-drive/tests/deterministic_root_hash.rs | 5 +- .../drive_abci_query_versions/mod.rs | 20 ++ .../drive_abci_query_versions/v0.rs | 14 +- .../drive_abci_query_versions/v1.rs | 14 +- .../drive_abci_query_versions/v3.rs | 17 +- .../drive_contract_method_versions/mod.rs | 3 + .../drive_contract_method_versions/v1.rs | 2 + .../drive_contract_method_versions/v2.rs | 2 + .../drive_contract_method_versions/v3.rs | 2 + .../drive_contract_method_versions/v4.rs | 24 ++ .../drive_verify_method_versions/mod.rs | 1 + .../drive_verify_method_versions/v1.rs | 1 + .../src/version/drive_versions/v9.rs | 4 +- .../src/version/mocks/v2_test.rs | 14 +- .../data_contracts_latest_versions.rs | 17 +- .../wasm-sdk/src/queries/data_contract.rs | 7 +- 39 files changed, 1704 insertions(+), 105 deletions(-) create mode 100644 packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/migration/add_version_items_to_all_contracts.rs create mode 100644 packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/version_item.rs create mode 100644 packages/rs-drive/src/verify/contract/verify_contracts_versions/mod.rs create mode 100644 packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs diff --git a/book/src/drive/indexes.md b/book/src/drive/indexes.md index bc8e026ae4a..347136c8c74 100644 --- a/book/src/drive/indexes.md +++ b/book/src/drive/indexes.md @@ -172,7 +172,7 @@ Let's break that down: - **`DataContractDocuments`** — root tree byte (`u8` constant) for "this is a document index, not a contract definition or identity record". - **`contract_id`** — 32-byte contract identifier. -- **`1`** — separator distinguishing the document storage area from the contract definition area within `contract_id`. +- **`1`** — separator distinguishing the document storage area from the contract definition area within `contract_id`. Its siblings under `contract_id` are `0`, the serialized contract (or, for a contract that keeps history, the history subtree whose key `0` references the latest revision), and, from protocol version 14, `2`, the contract's version number as a four-byte item that `getDataContractsLatestVersions` reads and proves without the contract. - **`doc_type_name`** — UTF-8 bytes of the document type (`"person"`, `"contactRequest"`, etc.). - **`propA_name, vA, propB_name, vB`** — alternating property key and serialized value, one pair per index property, in declaration order. - **`0`** — the conventional "terminal slot" byte under each value level; it's where the actual reference (or sub-tree-of-references) lives. diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 37c8445dac2..8066b4b555e 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -501,14 +501,20 @@ message GetDataContractResponse { oneof version { GetDataContractResponseV0 v0 = 1; } } -// Returns the current version number of each requested data contract. The -// unproved form is answered from Drive's contract cache and carries one -// integer per contract, which makes it the cheap way for a client to check -// that the contracts it already holds are still current; serialized -// contracts are added only when `include_contracts` is set. Every requested -// id gets an entry; an id no contract has gets an entry without `version`. -// With `prove`, the proof is the multi-contract proof `getDataContracts` -// returns, and it carries the contracts whatever `include_contracts` says. +// Returns the current version number of each requested data contract: the +// cheap way for a client to check that the contracts it already holds are +// still current. Every requested id gets an entry; an id no contract has gets +// an entry without `version`. Serialized contracts are added only when +// `include_contracts` is set. +// +// From protocol version 14 every contract carries a four-byte version item +// beside it in state. Without `include_contracts`, the unproved form answers +// from Drive's contract cache or that item without loading a contract, and +// the proof covers the items, a few hundred bytes of hash path per contract. +// With `include_contracts`, and on earlier protocol versions, the unproved +// form reads the contracts through the cache and the proof is the +// multi-contract proof `getDataContracts` returns, which carries the +// contracts. message GetDataContractsLatestVersionsRequest { message GetDataContractsLatestVersionsRequestV0 { repeated bytes ids = diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index c43a113b7e0..0fe2fdd917c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -753,6 +753,13 @@ impl Platform { &platform_version.drive, )?; + // Contract version items: from this version the storage writer stores every + // contract's version as a four-byte item beside it, and + // `getDataContractsLatestVersions` reads and proves that item instead of the + // contracts. Every contract stored before this block gets its item here. + self.drive + .add_version_items_to_all_contracts(transaction, platform_version)?; + Ok(()) } } @@ -1496,6 +1503,67 @@ mod tests { ); } + /// A chain born at protocol version 13 stores its contracts without version items; the + /// transition to 14 gives every stored contract the item holding its stored version. + #[test] + fn test_transition_to_version_14_writes_the_version_item_of_every_contract() { + use dpp::data_contract::accessors::v0::DataContractV0Getters; + + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(13) + .build_with_mock_rpc() + .set_genesis_state(); + + let dpns_id = SystemDataContract::DPNS.id().to_buffer(); + assert_eq!( + platform + .drive + .fetch_contract_version(dpns_id, None, platform_version) + .expect("expected to read the version item"), + None, + "a v13 genesis state has no version items" + ); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 1_000_000, + height: 100, + core_height: 10, + epoch: Epoch::default(), + }; + platform + .transition_to_version_14(&block_info, &transaction, platform_version) + .expect("expected the transition to succeed"); + + let mut checked = 0; + for system_contract in SystemDataContract::ALL { + let id = system_contract.id().to_buffer(); + let Some(stored) = platform + .drive + .fetch_contract(id, None, None, Some(&transaction), platform_version) + .value + .expect("expected to fetch the contract") + else { + // not every system contract is registered at a v13 genesis + continue; + }; + assert_eq!( + platform + .drive + .fetch_contract_version(id, Some(&transaction), platform_version) + .expect("expected to read the version item"), + Some(stored.contract.version()), + "{system_contract:?} has its version item after the transition" + ); + checked += 1; + } + assert!( + checked >= 3, + "expected the genesis system contracts to be checked" + ); + } + #[test] fn test_transition_to_version_14_creates_total_credits_history_tree() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs b/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs index 7e0b4092b3d..8b03b262dc8 100644 --- a/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; @@ -17,18 +18,23 @@ use dpp::version::PlatformVersion; use dpp::{check_validation_result_with_data, ProtocolError}; use drive::error::query::QuerySyntaxError; use drive::util::grove_operations::GroveDBToUse; +use std::collections::BTreeSet; impl Platform { /// Returns the current version number of each requested data contract, and the serialized /// contracts only when `include_contracts` is set. Every requested id gets an entry; an id /// no contract has gets an entry without a version. /// - /// The unproved form reads Drive's contract cache: a hit is a map lookup and the response - /// is one integer per contract, which makes it the cheap way for a client to check that - /// the contracts it holds are still current. A miss loads the contract from state into the - /// cache, as the document queries do. The proved form is the multi-contract proof - /// `getDataContracts` returns; that proof carries the contracts whatever - /// `include_contracts` says, because GroveDB proves an item together with its value. + /// From protocol version 14 every contract carries a four-byte version item beside it + /// (`latest_versions_read` helper version 1). Without `include_contracts`, the unproved + /// form answers from Drive's contract cache when it holds the contract and from the + /// version item otherwise, never loading a contract; the proved form proves the version + /// items, a few hundred bytes of hash path per contract instead of the contract. + /// + /// With `include_contracts`, and on state without version items (helper version 0), the + /// unproved form reads the contracts through the cache, loading a miss into it as the + /// document queries do, and the proved form is the multi-contract proof `getDataContracts` + /// returns, which carries the contracts. pub(super) fn query_data_contracts_latest_versions_v0( &self, GetDataContractsLatestVersionsRequestV0 { @@ -68,10 +74,35 @@ impl Platform { }) .collect::, QueryError>>()); + let from_version_items = match platform_version + .drive_abci + .query + .data_contract_query_helpers + .latest_versions_read + { + 0 => false, + 1 => !include_contracts, + version => { + return Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "query_data_contracts_latest_versions_v0::latest_versions_read" + .to_string(), + known_versions: vec![0, 1], + received: version, + })) + } + }; + let response = if prove { - let proof = + let proof = if from_version_items { + self.drive.prove_contracts_versions( + contract_ids.as_slice(), + None, + platform_version, + )? + } else { self.drive - .prove_contracts(contract_ids.as_slice(), None, platform_version)?; + .prove_contracts(contract_ids.as_slice(), None, platform_version)? + }; GetDataContractsLatestVersionsResponseV0 { result: Some( @@ -83,36 +114,18 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, CheckpointUsed::Current)), } } else { - let contracts = self.drive.get_contracts_with_fetch_info( - contract_ids.as_slice(), - true, - None, - platform_version, - )?; - - let entries = contracts - .into_iter() - .map(|(contract_id, maybe_contract_fetch_info)| { - let (version, data_contract) = match maybe_contract_fetch_info { - None => (None, None), - Some(contract_fetch_info) => { - let contract = &contract_fetch_info.contract; - let data_contract = include_contracts - .then(|| { - contract - .serialize_to_bytes_with_platform_version(platform_version) - }) - .transpose()?; - (Some(contract.version()), data_contract) - } - }; - Ok(DataContractLatestVersionEntry { - identifier: contract_id.to_vec(), - version, - data_contract, - }) - }) - .collect::, ProtocolError>>()?; + let entries = if from_version_items { + self.data_contracts_latest_versions_from_version_items( + contract_ids, + platform_version, + )? + } else { + self.data_contracts_latest_versions_from_contracts( + contract_ids, + include_contracts, + platform_version, + )? + }; GetDataContractsLatestVersionsResponseV0 { result: Some( @@ -126,6 +139,79 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } + + /// One entry per distinct id, in id order: the version from the contract cache when it + /// holds the contract, else from the version item beside the contract. A contract is + /// never loaded from state for this. + fn data_contracts_latest_versions_from_version_items( + &self, + contract_ids: Vec<[u8; 32]>, + platform_version: &PlatformVersion, + ) -> Result, Error> { + contract_ids + .into_iter() + .collect::>() + .into_iter() + .map(|contract_id| { + let version = match self.drive.get_cached_contract_with_fetch_info( + contract_id, + None, + &platform_version.drive, + )? { + Some(contract_fetch_info) => Some(contract_fetch_info.contract.version()), + None => { + self.drive + .fetch_contract_version(contract_id, None, platform_version)? + } + }; + Ok(DataContractLatestVersionEntry { + identifier: contract_id.to_vec(), + version, + data_contract: None, + }) + }) + .collect() + } + + /// One entry per distinct id, in id order, read from the contracts through the cache: a + /// miss loads the contract into the cache, as the document queries do. + fn data_contracts_latest_versions_from_contracts( + &self, + contract_ids: Vec<[u8; 32]>, + include_contracts: bool, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let contracts = self.drive.get_contracts_with_fetch_info( + contract_ids.as_slice(), + true, + None, + platform_version, + )?; + + contracts + .into_iter() + .map(|(contract_id, maybe_contract_fetch_info)| { + let (version, data_contract) = match maybe_contract_fetch_info { + None => (None, None), + Some(contract_fetch_info) => { + let contract = &contract_fetch_info.contract; + let data_contract = include_contracts + .then(|| { + contract.serialize_to_bytes_with_platform_version(platform_version) + }) + .transpose()?; + (Some(contract.version()), data_contract) + } + }; + Ok(DataContractLatestVersionEntry { + identifier: contract_id.to_vec(), + version, + data_contract, + }) + }) + .collect::, ProtocolError>>() + .map_err(Error::from) + } } #[cfg(test)] @@ -139,6 +225,7 @@ mod tests { use dpp::data_contract::DataContract; use dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructureTrusted; use dpp::tests::fixtures::get_data_contract_fixture; + use drive::drive::Drive; fn request( ids: Vec>, @@ -184,6 +271,21 @@ mod tests { } } + /// The proof of a successful proved response. + fn proof( + result: QueryValidationResult, + ) -> dapi_grpc::platform::v0::Proof { + assert!( + result.errors.is_empty(), + "unexpected errors {:?}", + result.errors + ); + match result.data.expect("expected data").result { + Some(get_data_contracts_latest_versions_response_v0::Result::Proof(proof)) => proof, + other => panic!("expected a proof, got {other:?}"), + } + } + #[test] fn test_invalid_data_contract_id() { let (platform, state, version) = setup_platform(None, Network::Testnet, None); @@ -400,4 +502,155 @@ mod tests { assert_eq!(after[0].version, Some(contract.version())); assert_eq!(after[0].version, before[0].version.map(|v| v + 1)); } + + /// Without the contracts, the proof covers the version items: it verifies as such, reports + /// the stored version and the absence, and is a small fraction of the multi-contract + /// proof the same request with the contracts returns. + #[test] + fn test_proof_without_contracts_covers_the_version_items() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = store_contract_with_id(&platform, [1; 32], version); + let ids = vec![vec![1; 32], vec![2; 32]]; + + let versions_proof = proof( + platform + .query_data_contracts_latest_versions_v0( + request(ids.clone(), false, true), + &state, + version, + ) + .expect("expected query to succeed"), + ); + let (_root_hash, versions) = Drive::verify_contracts_versions( + &versions_proof.grovedb_proof, + &[[1; 32], [2; 32]], + version, + ) + .expect("the proof verifies as a version item proof"); + assert_eq!(versions.get(&[1; 32]), Some(&Some(contract.version()))); + assert_eq!(versions.get(&[2; 32]), Some(&None)); + + let contracts_proof = proof( + platform + .query_data_contracts_latest_versions_v0(request(ids, true, true), &state, version) + .expect("expected query to succeed"), + ); + let (_root_hash, contracts) = Drive::verify_contracts( + &contracts_proof.grovedb_proof, + false, + &[[1; 32], [2; 32]], + version, + ) + .expect("with the contracts the proof is the multi-contract proof"); + assert_eq!( + contracts + .get(&[1; 32]) + .and_then(|contract| contract.as_ref()) + .map(|contract| contract.version()), + Some(contract.version()) + ); + assert!( + versions_proof.grovedb_proof.len() * 4 < contracts_proof.grovedb_proof.len(), + "the version item proof ({} bytes) must be well under a quarter of the contract proof ({} bytes)", + versions_proof.grovedb_proof.len(), + contracts_proof.grovedb_proof.len() + ); + } + + /// Without the contracts, the unproved answer comes from the version item on a cache + /// miss and never loads the contract; asking for the contracts does load it. + #[test] + fn test_unproved_without_contracts_does_not_load_the_contract() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let contract = store_contract_with_id(&platform, [1; 32], version); + let cached = || { + platform + .drive + .get_cached_contract_with_fetch_info([1; 32], None, &version.drive) + .expect("expected the cache lookup to succeed") + }; + assert!(cached().is_none(), "applying a contract does not cache it"); + + let entries_without = entries( + platform + .query_data_contracts_latest_versions_v0( + request(vec![vec![1; 32]], false, false), + &state, + version, + ) + .expect("expected query to succeed"), + ); + assert_eq!(entries_without[0].version, Some(contract.version())); + assert!( + cached().is_none(), + "the version came from the version item, not from loading the contract" + ); + + let entries_with = entries( + platform + .query_data_contracts_latest_versions_v0( + request(vec![vec![1; 32]], true, false), + &state, + version, + ) + .expect("expected query to succeed"), + ); + assert!(entries_with[0].data_contract.is_some()); + assert!( + cached().is_some(), + "asking for the contract loads it into the cache" + ); + } + + /// Before protocol version 14 there are no version items: without the contracts the + /// proof is still the multi-contract proof, and the unproved answer loads the contract. + #[test] + fn test_proof_without_contracts_is_the_contract_proof_before_protocol_version_14() { + let (platform, state, version) = setup_platform(None, Network::Testnet, Some(13)); + assert_eq!(version.protocol_version, 13); + let contract = store_contract_with_id(&platform, [1; 32], version); + + let proof = proof( + platform + .query_data_contracts_latest_versions_v0( + request(vec![vec![1; 32]], false, true), + &state, + version, + ) + .expect("expected query to succeed"), + ); + let (_root_hash, contracts) = + Drive::verify_contracts(&proof.grovedb_proof, false, &[[1; 32]], version) + .expect("the proof is the multi-contract proof"); + assert_eq!( + contracts + .get(&[1; 32]) + .and_then(|contract| contract.as_ref()) + .map(|contract| contract.version()), + Some(contract.version()) + ); + assert!( + Drive::verify_contracts_versions(&proof.grovedb_proof, &[[1; 32]], version).is_err(), + "no version item proof can be read out of it" + ); + + let entries = entries( + platform + .query_data_contracts_latest_versions_v0( + request(vec![vec![1; 32]], false, false), + &state, + version, + ) + .expect("expected query to succeed"), + ); + assert_eq!(entries[0].version, Some(contract.version())); + assert!( + platform + .drive + .get_cached_contract_with_fetch_info([1; 32], None, &version.drive) + .expect("expected the cache lookup to succeed") + .is_some(), + "before the version items the unproved answer loads the contract" + ); + } } diff --git a/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs b/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs index dbbbf270968..73a0343512f 100644 --- a/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs +++ b/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs @@ -49,37 +49,79 @@ impl FromProof for DataContractsLatestVer }) .collect::, Error>>()?; - // The proof is the multi-contract proof, which carries the contracts: each version is - // read off the verified contract, and the contract is kept only when it was asked for. - let (root_hash, contracts) = Drive::verify_contracts( - supported_grovedb_proof_bytes(proof, platform_version)?, - false, - ids.as_slice(), - platform_version, - ) - .map_drive_error(proof, mtd)?; - - verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; - - let versions = contracts - .into_iter() - .map(|(id, maybe_contract)| { - let id = Identifier::from_bytes(&id).map_err(|e| Error::ResultEncodingError { - error: e.to_string(), - })?; - let entry = maybe_contract.map(|contract| DataContractLatestVersion { - version: contract.version(), - data_contract: include_contracts.then_some(contract), - }); - Ok((id, entry)) - }) - .collect::>()?; + // From protocol version 14 (helper version 1) every contract carries a version item, + // and a request without the contracts is answered by a proof of those items. Before + // that, and whenever the contracts were asked for, the proof is the multi-contract + // proof, which carries the contracts: each version is read off the verified contract, + // and the contract is kept only when it was asked for. + let from_version_items = match platform_version + .drive_abci + .query + .data_contract_query_helpers + .latest_versions_read + { + 0 => false, + 1 => !include_contracts, + version => { + return Err(Error::DriveError { + error: format!( + "unknown data contract latest versions helper version {version}" + ), + }) + } + }; + + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + + let versions = if from_version_items { + let (root_hash, versions) = + Drive::verify_contracts_versions(proof_bytes, ids.as_slice(), platform_version) + .map_drive_error(proof, mtd)?; + + verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; + + versions + .into_iter() + .map(|(id, maybe_version)| { + let id = identifier_from_bytes(&id)?; + let entry = maybe_version.map(|version| DataContractLatestVersion { + version, + data_contract: None, + }); + Ok((id, entry)) + }) + .collect::>()? + } else { + let (root_hash, contracts) = + Drive::verify_contracts(proof_bytes, false, ids.as_slice(), platform_version) + .map_drive_error(proof, mtd)?; + + verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; + + contracts + .into_iter() + .map(|(id, maybe_contract)| { + let id = identifier_from_bytes(&id)?; + let entry = maybe_contract.map(|contract| DataContractLatestVersion { + version: contract.version(), + data_contract: include_contracts.then_some(contract), + }); + Ok((id, entry)) + }) + .collect::>()? + }; // Every requested id has an entry, so the result is never absent. Ok((Some(versions), mtd.clone(), proof.clone())) } } +fn identifier_from_bytes(bytes: &[u8; 32]) -> Result { + Identifier::from_bytes(bytes).map_err(|e| Error::ResultEncodingError { + error: e.to_string(), + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs b/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs index 2bac7b5b795..d1f16b2c60a 100644 --- a/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs +++ b/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs @@ -6,8 +6,8 @@ use dpp::identifier::Identifier; /// /// `data_contract` is `Some` only when the request set `include_contracts`. The query exists /// so that a client can check the contracts it already holds against `version` without -/// transferring them again; the unproved form is answered from Drive's contract cache with -/// one integer per contract. +/// transferring them again: from protocol version 14 a request without the contracts is +/// answered from, and proved by, the four-byte version item each contract carries in state. #[derive(Debug, Clone, PartialEq)] pub struct DataContractLatestVersion { /// The contract's current version number. diff --git a/packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/mod.rs b/packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/mod.rs new file mode 100644 index 00000000000..34152bee33a --- /dev/null +++ b/packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/mod.rs @@ -0,0 +1,52 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Reads a contract's version from the version item stored beside it (`[64, id] / 2`) + /// without deserializing the contract. + /// + /// The item exists from protocol version 14 (written on every contract create and + /// update, and backfilled on the version's first block). Before that, and for an id no + /// contract has, the result is `None`. + /// + /// # Arguments + /// + /// * `contract_id` - The contract id. + /// * `transaction` - The transaction to read in, or `None` for the committed state. + /// * `platform_version` - The platform version for version dispatch. + /// + /// # Returns + /// + /// The stored version number, or `None` when there is no version item. + /// + /// # Errors + /// + /// Returns an error if the item is not a four-byte item, the read fails, or the method + /// version is unknown. + pub fn fetch_contract_version( + &self, + contract_id: [u8; 32], + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .contract + .get + .fetch_contract_version + { + 0 => self.fetch_contract_version_v0(contract_id, transaction, platform_version), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_contract_version".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/v0/mod.rs b/packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/v0/mod.rs new file mode 100644 index 00000000000..cc5994bffca --- /dev/null +++ b/packages/rs-drive/src/drive/contract/get_fetch/fetch_contract_version/v0/mod.rs @@ -0,0 +1,70 @@ +use crate::drive::contract::paths::{contract_root_path, CONTRACT_VERSION_KEY}; +use crate::drive::contract::version_item::decode_contract_version; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::util::grove_operations::DirectQueryType; +use dpp::version::PlatformVersion; +use grovedb::Element::Item; +use grovedb::TransactionArg; + +impl Drive { + /// Reads the version item under the contract's root subtree. A missing contract subtree + /// (an id no contract has) reads as no item, like a contract stored before the item + /// existed. + #[inline(always)] + pub(super) fn fetch_contract_version_v0( + &self, + contract_id: [u8; 32], + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let contract_root_path = contract_root_path(&contract_id); + + match self.grove_get_raw_optional( + (&contract_root_path).into(), + &[CONTRACT_VERSION_KEY], + DirectQueryType::StatefulDirectQuery, + transaction, + &mut vec![], + &platform_version.drive, + ) { + Ok(Some(Item(encoded_version, _))) => decode_contract_version(&encoded_version) + .map(Some) + .ok_or(Error::Drive(DriveError::CorruptedElementType( + "contract version item was not 4 bytes as expected", + ))), + Ok(Some(..)) => Err(Error::Drive(DriveError::CorruptedElementType( + "contract version was present but was not an item", + ))), + Ok(None) => Ok(None), + Err(Error::GroveDB(error)) + if matches!( + *error, + grovedb::Error::PathParentLayerNotFound(_) | grovedb::Error::PathNotFound(_) + ) => + { + Ok(None) + } + Err(e) => Err(e), + } + } +} + +#[cfg(test)] +mod tests { + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::version::PlatformVersion; + + #[test] + fn should_read_no_version_for_an_unknown_contract() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let version = drive + .fetch_contract_version([9; 32], None, platform_version) + .expect("an unknown contract reads as no version"); + + assert_eq!(version, None); + } +} diff --git a/packages/rs-drive/src/drive/contract/get_fetch/mod.rs b/packages/rs-drive/src/drive/contract/get_fetch/mod.rs index be0af692c34..90b976a82c2 100644 --- a/packages/rs-drive/src/drive/contract/get_fetch/mod.rs +++ b/packages/rs-drive/src/drive/contract/get_fetch/mod.rs @@ -1,5 +1,6 @@ mod fetch_contract; mod fetch_contract_ids; +mod fetch_contract_version; mod fetch_contract_with_history; mod fetch_contracts; mod get_cached_contract_with_fetch_info; diff --git a/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/mod.rs b/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/mod.rs index 66a55d37126..da1672528e8 100644 --- a/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::drive::LowLevelDriveOperation; @@ -45,9 +46,19 @@ impl Drive { transaction, drive_version, ), + 1 => self.add_contract_to_storage_v1( + contract_element, + contract, + block_info, + estimated_costs_only_with_layer_info, + insert_operations, + is_first_insert, + transaction, + drive_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_contract_to_storage".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/v1/mod.rs b/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/v1/mod.rs new file mode 100644 index 00000000000..311f10ed9be --- /dev/null +++ b/packages/rs-drive/src/drive/contract/insert/add_contract_to_storage/v1/mod.rs @@ -0,0 +1,225 @@ +use crate::drive::contract::paths::{contract_root_path, CONTRACT_VERSION_KEY}; +use crate::drive::contract::version_item::encode_contract_version; +use crate::drive::Drive; +use crate::drive::LowLevelDriveOperation; +use crate::error::Error; +use crate::util::object_size_info::PathKeyElementInfo::{ + PathFixedSizeKeyRefElement, PathKeyElementSize, +}; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::DataContract; +use dpp::version::drive_versions::DriveVersion; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::KeyInfoPath; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Adds a contract to storage as v0 does, then writes the contract's version number as a + /// four-byte item beside it, at key `2` of the contract's root subtree, whether the + /// contract keeps history or not. + /// + /// The item is written on the first insert and overwritten on every update, so it always + /// holds the version of the stored contract (for a contract that keeps history, of its + /// latest revision). It carries the contract element's flags, so its storage is paid for + /// and refunded with the contract's. `getDataContractsLatestVersions` reads and proves + /// this item instead of the serialized contract. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn add_contract_to_storage_v1( + &self, + contract_element: Element, + contract: &DataContract, + block_info: &BlockInfo, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + insert_operations: &mut Vec, + is_first_insert: bool, + transaction: TransactionArg, + drive_version: &DriveVersion, + ) -> Result<(), Error> { + let element_flags = contract_element.get_flags().clone(); + + self.add_contract_to_storage_v0( + contract_element, + contract, + block_info, + estimated_costs_only_with_layer_info, + insert_operations, + is_first_insert, + transaction, + drive_version, + )?; + + let version_element = + Element::Item(encode_contract_version(contract.version()), element_flags); + let contract_root_path = contract_root_path(contract.id_ref().as_bytes()); + + let path_key_element_info = if estimated_costs_only_with_layer_info.is_none() { + PathFixedSizeKeyRefElement(( + contract_root_path, + &[CONTRACT_VERSION_KEY], + version_element, + )) + } else { + PathKeyElementSize(( + KeyInfoPath::from_known_path(contract_root_path), + KeyInfo::KnownKey(vec![CONTRACT_VERSION_KEY]), + version_element, + )) + }; + + self.batch_insert(path_key_element_info, insert_operations, drive_version) + } +} + +#[cfg(test)] +mod tests { + use crate::drive::Drive; + 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, DataContractV0Setters}; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::DataContract; + use dpp::tests::fixtures::get_dashpay_contract_fixture; + use dpp::version::PlatformVersion; + + fn dashpay_contract(keeps_history: bool, platform_version: &PlatformVersion) -> DataContract { + let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.config_mut().set_readonly(false); + contract.config_mut().set_keeps_history(keeps_history); + contract + } + + fn block(time_ms: u64) -> BlockInfo { + BlockInfo { + time_ms, + height: 100, + core_height: 10, + epoch: Default::default(), + } + } + + fn apply( + drive: &Drive, + contract: &DataContract, + time_ms: u64, + platform_version: &PlatformVersion, + ) { + drive + .apply_contract( + contract, + block(time_ms), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply the contract"); + } + + fn stored_version( + drive: &Drive, + contract: &DataContract, + platform_version: &PlatformVersion, + ) -> Option { + drive + .fetch_contract_version(contract.id().to_buffer(), None, platform_version) + .expect("expected to read the contract version item") + } + + #[test] + fn should_write_the_version_item_on_insert_and_keep_it_current_on_update() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + for keeps_history in [false, true] { + let mut contract = dashpay_contract(keeps_history, platform_version); + contract.set_id([keeps_history as u8 + 1; 32].into()); + + apply(&drive, &contract, 1000, platform_version); + assert_eq!( + stored_version(&drive, &contract, platform_version), + Some(contract.version()), + "keeps_history={keeps_history}: the item holds the inserted version" + ); + + contract.increment_version(); + apply(&drive, &contract, 2000, platform_version); + assert_eq!( + stored_version(&drive, &contract, platform_version), + Some(contract.version()), + "keeps_history={keeps_history}: the item follows the update" + ); + + let fetched = drive + .fetch_contract( + contract.id().to_buffer(), + None, + None, + None, + platform_version, + ) + .value + .expect("expected to fetch the contract") + .expect("expected the contract to exist"); + assert_eq!( + fetched.contract.version(), + contract.version(), + "the contract itself was updated too" + ); + } + } + + #[test] + fn should_not_write_the_version_item_before_protocol_version_14() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + let contract = dashpay_contract(false, platform_version); + + apply(&drive, &contract, 1000, platform_version); + + assert_eq!(stored_version(&drive, &contract, platform_version), None); + } + + #[test] + fn should_estimate_an_insert_with_the_version_item() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let contract = dashpay_contract(false, platform_version); + + let estimated_fee = drive + .insert_contract( + &contract, + BlockInfo::default(), + false, + None, + platform_version, + ) + .expect("expected the stateless insert estimate to succeed"); + let actual_fee = drive + .insert_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected the insert to succeed"); + + assert!( + estimated_fee.storage_fee >= actual_fee.storage_fee, + "the estimate ({}) must not undershoot the actual storage fee ({})", + estimated_fee.storage_fee, + actual_fee.storage_fee + ); + assert_eq!( + stored_version(&drive, &contract, platform_version), + Some(contract.version()) + ); + } +} diff --git a/packages/rs-drive/src/drive/contract/migration/add_version_items_to_all_contracts.rs b/packages/rs-drive/src/drive/contract/migration/add_version_items_to_all_contracts.rs new file mode 100644 index 00000000000..e5ed7e960cc --- /dev/null +++ b/packages/rs-drive/src/drive/contract/migration/add_version_items_to_all_contracts.rs @@ -0,0 +1,245 @@ +use crate::drive::contract::paths::{contract_root_path, CONTRACT_VERSION_KEY}; +use crate::drive::contract::version_item::encode_contract_version; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::util::storage_flags::StorageFlags; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::version::PlatformVersion; +use grovedb::{Element, Transaction}; + +impl Drive { + /// Writes the version item (`[64, id] / 2`, the version as four big-endian bytes) of + /// every contract in state. + /// + /// Runs once, on the first block of protocol version 14: from that version the storage + /// writer keeps the item current on every contract create and update, and + /// `getDataContractsLatestVersions` reads and proves it instead of the contracts, so every + /// contract stored before the version needs one. Each item carries its contract's element + /// flags, so it is accounted to the contract's owner like the contract itself. Writing an + /// item a contract already has is a no-op, so the walk is safe to repeat. + pub fn add_version_items_to_all_contracts( + &self, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut start_at = None; + let mut contract_count = 0usize; + + loop { + let page = + self.fetch_contract_ids(start_at, u16::MAX, Some(transaction), platform_version)?; + + for contract_id in &page { + self.add_version_item_to_contract(*contract_id, transaction, platform_version)?; + } + contract_count += page.len(); + + match page.last() { + Some(last_id) if page.len() == u16::MAX as usize => { + start_at = Some((*last_id, false)); + } + _ => break, + } + } + + tracing::info!( + contract_count, + "wrote the version item of every contract in state" + ); + + Ok(()) + } + + fn add_version_item_to_contract( + &self, + contract_id: [u8; 32], + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let fetch_info = self + .fetch_contract_and_add_operations( + contract_id, + None, + Some(transaction), + &mut vec![], + platform_version, + )? + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState(format!( + "contract {} is listed under the contracts root but can not be fetched", + hex::encode(contract_id) + ))) + })?; + + let element_flags = fetch_info + .storage_flags + .as_ref() + .map(StorageFlags::to_element_flags); + let version_element = Element::Item( + encode_contract_version(fetch_info.contract.version()), + element_flags, + ); + let contract_root_path = contract_root_path(&contract_id); + + self.grove_insert( + (&contract_root_path).into(), + &[CONTRACT_VERSION_KEY], + version_element, + Some(transaction), + None, + &mut vec![], + &platform_version.drive, + ) + } +} + +#[cfg(test)] +mod tests { + use crate::drive::Drive; + 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, DataContractV0Setters}; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::DataContract; + use dpp::tests::json_document::json_document_to_contract; + use dpp::version::PlatformVersion; + use grovedb_epoch_based_storage_flags::StorageFlags; + + const PLAIN_CONTRACT: &str = "tests/supporting_files/contract/family/family-contract.json"; + const HISTORY_CONTRACT: &str = + "tests/supporting_files/contract/references/references_with_contract_history.json"; + + fn apply(drive: &Drive, contract: &DataContract, platform_version: &PlatformVersion) { + drive + .apply_contract( + contract, + BlockInfo { + time_ms: 1000, + height: 100, + core_height: 10, + epoch: Default::default(), + }, + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply the contract"); + } + + /// Stores contracts of both layouts under protocol version 13, where no version item is + /// written. + fn setup_pre_upgrade_contracts(drive: &Drive) -> Vec { + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + [(1u8, false), (2, true), (3, false)] + .iter() + .map(|(seed, keeps_history)| { + let fixture = if *keeps_history { + HISTORY_CONTRACT + } else { + PLAIN_CONTRACT + }; + let mut contract = json_document_to_contract(fixture, false, platform_version) + .expect("expected to load the contract fixture"); + contract.set_id([*seed; 32].into()); + contract.set_version(*seed as u32 + 4); + if *keeps_history { + contract.config_mut().set_keeps_history(true); + contract.config_mut().set_readonly(false); + } + apply(drive, &contract, platform_version); + contract + }) + .collect() + } + + fn stored_version(drive: &Drive, contract: &DataContract) -> Option { + drive + .fetch_contract_version(contract.id().to_buffer(), None, PlatformVersion::latest()) + .expect("expected to read the version item") + } + + #[test] + fn should_write_the_version_item_of_every_contract_stored_before_the_upgrade() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let contracts = setup_pre_upgrade_contracts(&drive); + for contract in &contracts { + assert_eq!( + stored_version(&drive, contract), + None, + "no item before the upgrade" + ); + } + + let transaction = drive.grove.start_transaction(); + drive + .add_version_items_to_all_contracts(&transaction, platform_version) + .expect("expected the backfill to succeed"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + for contract in &contracts { + assert_eq!( + stored_version(&drive, contract), + Some(contract.version()), + "the item holds the stored version" + ); + } + } + + #[test] + fn should_leave_contracts_that_already_have_the_item_unchanged() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let mut contracts = setup_pre_upgrade_contracts(&drive); + + // A contract written under protocol version 14 already carries its item. + let upgraded = &mut contracts[0]; + upgraded.increment_version(); + apply(&drive, upgraded, platform_version); + assert_eq!(stored_version(&drive, upgraded), Some(upgraded.version())); + + let root_hash_before_backfill = |drive: &Drive| { + drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("expected a root hash") + }; + + let transaction = drive.grove.start_transaction(); + drive + .add_version_items_to_all_contracts(&transaction, platform_version) + .expect("expected the backfill to succeed"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + let root_hash_after_first = root_hash_before_backfill(&drive); + + let transaction = drive.grove.start_transaction(); + drive + .add_version_items_to_all_contracts(&transaction, platform_version) + .expect("expected the repeated backfill to succeed"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + assert_eq!( + root_hash_before_backfill(&drive), + root_hash_after_first, + "repeating the backfill changes nothing" + ); + for contract in &contracts { + assert_eq!(stored_version(&drive, contract), Some(contract.version())); + } + } +} diff --git a/packages/rs-drive/src/drive/contract/migration/mod.rs b/packages/rs-drive/src/drive/contract/migration/mod.rs index df8449cd693..4bf094f6ad0 100644 --- a/packages/rs-drive/src/drive/contract/migration/mod.rs +++ b/packages/rs-drive/src/drive/contract/migration/mod.rs @@ -1 +1,2 @@ +mod add_version_items_to_all_contracts; mod strip_unknown_document_schema_properties; diff --git a/packages/rs-drive/src/drive/contract/mod.rs b/packages/rs-drive/src/drive/contract/mod.rs index fb24adfbd74..6d1ede744aa 100644 --- a/packages/rs-drive/src/drive/contract/mod.rs +++ b/packages/rs-drive/src/drive/contract/mod.rs @@ -29,6 +29,8 @@ mod refresh_cache; pub mod test_helpers; #[cfg(feature = "server")] mod update; +#[cfg(any(feature = "server", feature = "verify"))] +pub mod version_item; #[cfg(feature = "server")] pub use contract_fetch_info::*; #[cfg(feature = "server")] diff --git a/packages/rs-drive/src/drive/contract/paths.rs b/packages/rs-drive/src/drive/contract/paths.rs index 0baa4d5a226..3bc40836d2d 100644 --- a/packages/rs-drive/src/drive/contract/paths.rs +++ b/packages/rs-drive/src/drive/contract/paths.rs @@ -165,3 +165,9 @@ pub fn contract_keeping_history_storage_time_reference_path( encoded_time, ] } + +/// The key under a contract's root subtree (`[64, id]`) that holds the contract's version +/// number as a four-byte big-endian item, written beside the contract from protocol +/// version 14. Keys `0` (the contract, or its history subtree) and `1` (the documents) are +/// the other children of that subtree. +pub const CONTRACT_VERSION_KEY: u8 = 2; diff --git a/packages/rs-drive/src/drive/contract/prove/mod.rs b/packages/rs-drive/src/drive/contract/prove/mod.rs index b22219c392a..def39ded2bc 100644 --- a/packages/rs-drive/src/drive/contract/prove/mod.rs +++ b/packages/rs-drive/src/drive/contract/prove/mod.rs @@ -9,3 +9,6 @@ mod prove_contracts; /// The `prove_contracts_by_range` module proves one page of the contract enumeration. mod prove_contracts_by_range; + +/// The `prove_contracts_versions` module proves the version items of several contracts. +mod prove_contracts_versions; diff --git a/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/mod.rs b/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/mod.rs new file mode 100644 index 00000000000..5dff043449a --- /dev/null +++ b/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/mod.rs @@ -0,0 +1,56 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; + +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Proves the version items of the specified contracts: for each requested id, either + /// the four-byte version item stored beside the contract or its absence. + /// + /// This is the proof `getDataContractsLatestVersions` returns when the contracts + /// themselves are not requested, from protocol version 14 (state before that carries no + /// version items). It is a few hundred bytes of hash path per contract instead of the + /// serialized contract the multi-contract proof carries. `Drive::verify_contracts_versions` + /// verifies it. + /// + /// # Arguments + /// + /// * `contract_ids` - The contract ids whose versions to prove, at least one; duplicates + /// are folded. + /// * `transaction` - The transaction to prove against, or `None` for the committed state. + /// * `platform_version` - The platform version for version dispatch. + /// + /// # Returns + /// + /// The GroveDB proof bytes. + /// + /// # Errors + /// + /// Returns an error if no ids were given, the proof generation fails, or the method + /// version is unknown. + pub fn prove_contracts_versions( + &self, + contract_ids: &[[u8; 32]], + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .contract + .prove + .prove_contracts_versions + { + 0 => self.prove_contracts_versions_v0(contract_ids, transaction, platform_version), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "prove_contracts_versions".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs b/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs new file mode 100644 index 00000000000..134c430d922 --- /dev/null +++ b/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs @@ -0,0 +1,31 @@ +use crate::drive::Drive; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; + +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + #[inline(always)] + pub(super) fn prove_contracts_versions_v0( + &self, + contract_ids: &[[u8; 32]], + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + if contract_ids.is_empty() { + return Err(Error::Query(QuerySyntaxError::NoQueryItems( + "no contract ids to prove versions for", + ))); + } + + let path_query = Self::fetch_contracts_versions_query(contract_ids); + + self.grove_get_proved_path_query( + &path_query, + transaction, + &mut vec![], + &platform_version.drive, + ) + } +} diff --git a/packages/rs-drive/src/drive/contract/queries.rs b/packages/rs-drive/src/drive/contract/queries.rs index eeadd7ca48c..8f1c7cc4460 100644 --- a/packages/rs-drive/src/drive/contract/queries.rs +++ b/packages/rs-drive/src/drive/contract/queries.rs @@ -1,5 +1,5 @@ use crate::drive::contract::paths::{ - contract_keeping_history_root_path_vec, contract_root_path_vec, + contract_keeping_history_root_path_vec, contract_root_path_vec, CONTRACT_VERSION_KEY, }; use crate::drive::contract::{paths, MAX_CONTRACT_HISTORY_FETCH_LIMIT}; use crate::drive::{Drive, RootTree}; @@ -9,6 +9,7 @@ use crate::query::{Query, QueryItem}; use crate::util::common::encode::encode_u64; use grovedb::{PathQuery, SizedQuery}; use platform_version::version::PlatformVersion; +use std::collections::BTreeSet; use std::ops::RangeFull; impl Drive { @@ -85,6 +86,30 @@ impl Drive { ) } + /// Creates the path query that proves the version items of the given contracts + /// (`getDataContractsLatestVersions` without the contracts), from protocol version 14. + /// + /// It selects the requested contract ids under the contracts root and descends the + /// subquery key `2`, the four-byte version item every contract carries beside its + /// serialized form or history subtree. A requested id no contract has is proved absent. + /// Duplicate ids are folded, so the limit is the number of distinct ids. + /// + /// Shared by the prover and the verifier, which must rebuild the exact query. + /// + /// # Arguments + /// + /// * `contract_ids` - The contract ids whose versions to prove, at least one. + pub fn fetch_contracts_versions_query(contract_ids: &[[u8; 32]]) -> PathQuery { + let distinct_ids: BTreeSet<&[u8; 32]> = contract_ids.iter().collect(); + let mut query = Query::new(); + query.insert_keys(distinct_ids.iter().map(|key| key.to_vec()).collect()); + query.set_subquery_key(vec![CONTRACT_VERSION_KEY]); + PathQuery::new( + vec![Into::<&[u8; 1]>::into(RootTree::DataContractDocuments).to_vec()], + SizedQuery::new(query, Some(distinct_ids.len() as u16), None), + ) + } + /// Creates a path query for a specified contract. /// /// This function takes a contract ID and creates a path query for fetching the contract data. diff --git a/packages/rs-drive/src/drive/contract/version_item.rs b/packages/rs-drive/src/drive/contract/version_item.rs new file mode 100644 index 00000000000..9f60eecd795 --- /dev/null +++ b/packages/rs-drive/src/drive/contract/version_item.rs @@ -0,0 +1,45 @@ +//! The contract version item: the value stored at [`CONTRACT_VERSION_KEY`] under a +//! contract's root subtree from protocol version 14, and what `getDataContractsLatestVersions` +//! reads and proves when the contracts themselves are not requested. + +use crate::drive::contract::paths::CONTRACT_VERSION_KEY; + +/// The size of a contract version item's value: the version as four big-endian bytes. +pub const CONTRACT_VERSION_ITEM_SIZE: usize = 4; + +/// Encodes a contract version as the value of its version item. +pub fn encode_contract_version(version: u32) -> Vec { + version.to_be_bytes().to_vec() +} + +/// Decodes the value of a contract version item, `None` when it is not four bytes long. +pub fn decode_contract_version(bytes: &[u8]) -> Option { + let bytes: [u8; CONTRACT_VERSION_ITEM_SIZE] = bytes.try_into().ok()?; + Some(u32::from_be_bytes(bytes)) +} + +/// The key of the version item as a one-byte slice, for path queries and raw reads. +pub const fn contract_version_key() -> [u8; 1] { + [CONTRACT_VERSION_KEY] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_round_trip_a_contract_version() { + for version in [0, 1, 7, u32::MAX] { + let encoded = encode_contract_version(version); + assert_eq!(encoded.len(), CONTRACT_VERSION_ITEM_SIZE); + assert_eq!(decode_contract_version(&encoded), Some(version)); + } + } + + #[test] + fn should_reject_a_value_that_is_not_four_bytes() { + assert_eq!(decode_contract_version(&[]), None); + assert_eq!(decode_contract_version(&[0, 0, 1]), None); + assert_eq!(decode_contract_version(&[0, 0, 0, 0, 1]), None); + } +} diff --git a/packages/rs-drive/src/verify/contract/mod.rs b/packages/rs-drive/src/verify/contract/mod.rs index 7a5038d44f4..e1784bf2068 100644 --- a/packages/rs-drive/src/verify/contract/mod.rs +++ b/packages/rs-drive/src/verify/contract/mod.rs @@ -4,6 +4,7 @@ mod verify_contract; mod verify_contract_history; mod verify_contract_return_serialization; mod verify_contracts_by_range; +mod verify_contracts_versions; fn retry_contract_verification_with_history( result: Result, diff --git a/packages/rs-drive/src/verify/contract/verify_contracts_versions/mod.rs b/packages/rs-drive/src/verify/contract/verify_contracts_versions/mod.rs new file mode 100644 index 00000000000..34140fbaa5d --- /dev/null +++ b/packages/rs-drive/src/verify/contract/verify_contracts_versions/mod.rs @@ -0,0 +1,57 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::verify::RootHash; +use dpp::version::PlatformVersion; +use std::collections::BTreeMap; + +/// The verified versions of the requested contracts, keyed by contract id: `None` for an id +/// no contract has. +pub type DataContractsVersions = BTreeMap<[u8; 32], Option>; + +impl Drive { + /// Verifies a proof of the version items of several contracts, the proof + /// `getDataContractsLatestVersions` returns without the contracts from protocol version + /// 14 (`Drive::prove_contracts_versions`). + /// + /// The proved query is rebuilt from the requested ids, so the caller must pass exactly + /// the ids it requested; duplicates are folded the way the prover folds them. + /// + /// # Parameters + /// + /// - `proof`: The GroveDB proof bytes. + /// - `contract_ids`: The requested contract ids, at least one. + /// - `platform_version`: The platform version for version dispatch. + /// + /// # Returns + /// + /// The root hash and one entry per distinct requested id: `Some(version)` for a contract + /// in state, `None` for an id no contract has. + /// + /// # Errors + /// + /// Returns an error if the proof is corrupted, does not cover exactly the requested ids, + /// or the method version is unknown. + pub fn verify_contracts_versions( + proof: &[u8], + contract_ids: &[[u8; 32]], + platform_version: &PlatformVersion, + ) -> Result<(RootHash, DataContractsVersions), Error> { + match platform_version + .drive + .methods + .verify + .contract + .verify_contracts_versions + { + 0 => Drive::verify_contracts_versions_v0(proof, contract_ids, platform_version), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_contracts_versions".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs b/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs new file mode 100644 index 00000000000..80f81eb3bbc --- /dev/null +++ b/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs @@ -0,0 +1,310 @@ +use super::DataContractsVersions; +use crate::drive::contract::paths::{all_contracts_global_root_path, CONTRACT_VERSION_KEY}; +use crate::drive::contract::version_item::decode_contract_version; +use crate::drive::Drive; +use crate::error::proof::ProofError; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::verify::RootHash; +use dpp::version::PlatformVersion; +use grovedb::{Element, GroveDb}; +use std::collections::BTreeSet; + +impl Drive { + /// Verifies the version item proof. + /// + /// Rebuilds the exact path query the prover used, verifies the proof against it with + /// absence, then reads every proved row back. A row is either the version item (or its + /// absence) at key `2` under the contract's root subtree, or the absence of the contract + /// subtree itself directly under the contracts root; both absences mean there is no + /// contract with that id. Every distinct requested id must appear exactly once and + /// nothing else may. + #[inline(always)] + pub(super) fn verify_contracts_versions_v0( + proof: &[u8], + contract_ids: &[[u8; 32]], + platform_version: &PlatformVersion, + ) -> Result<(RootHash, DataContractsVersions), Error> { + if contract_ids.is_empty() { + return Err(Error::Query(QuerySyntaxError::NoQueryItems( + "no contract ids to verify versions for", + ))); + } + + let requested: BTreeSet<[u8; 32]> = contract_ids.iter().copied().collect(); + let path_query = Self::fetch_contracts_versions_query(contract_ids); + + let (root_hash, proved_key_values) = GroveDb::verify_query_with_absence_proof( + proof, + &path_query, + &platform_version.drive.grove_version, + )?; + + let [contracts_root] = all_contracts_global_root_path(); + let mut versions = DataContractsVersions::new(); + + for (path, key, maybe_element) in proved_key_values { + let (contract_id, version) = match path.as_slice() { + [root, contract_id] if root.as_slice() == contracts_root => { + if key.as_slice() != [CONTRACT_VERSION_KEY] { + return Err(Error::Proof(ProofError::CorruptedProof( + "a contract version row is not keyed by the version key".to_string(), + ))); + } + ( + contract_id_from_bytes(contract_id)?, + maybe_element.map(version_from_element).transpose()?, + ) + } + [root] if root.as_slice() == contracts_root => { + if maybe_element.is_some() { + return Err(Error::Proof(ProofError::CorruptedProof( + "a contract version proof carries an element directly under the \ + contracts root" + .to_string(), + ))); + } + (contract_id_from_bytes(&key)?, None) + } + _ => { + return Err(Error::Proof(ProofError::CorruptedProof( + "a contract version row has an unexpected path".to_string(), + ))) + } + }; + + if !requested.contains(&contract_id) { + return Err(Error::Proof(ProofError::CorruptedProof( + "the contract version proof carries a contract that was not requested" + .to_string(), + ))); + } + if versions.insert(contract_id, version).is_some() { + return Err(Error::Proof(ProofError::CorruptedProof( + "the contract version proof carries a contract twice".to_string(), + ))); + } + } + + if versions.len() != requested.len() { + return Err(Error::Proof(ProofError::WrongElementCount { + expected: requested.len(), + got: versions.len(), + })); + } + + Ok((root_hash, versions)) + } +} + +/// Reads the version out of a proved version item. +fn version_from_element(element: Element) -> Result { + let bytes = element.into_item_bytes().map_err(Error::from)?; + decode_contract_version(&bytes).ok_or_else(|| { + Error::Proof(ProofError::CorruptedProof( + "a contract version item is not four bytes".to_string(), + )) + }) +} + +fn contract_id_from_bytes(bytes: &[u8]) -> Result<[u8; 32], Error> { + bytes.try_into().map_err(|_| { + Error::Proof(ProofError::CorruptedProof(format!( + "a contract id in the contract version proof has {} bytes, expected 32", + bytes.len() + ))) + }) +} + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use crate::drive::Drive; + use crate::error::Error; + 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, DataContractV0Setters}; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::DataContract; + use dpp::tests::json_document::json_document_to_contract; + use dpp::version::PlatformVersion; + use grovedb_epoch_based_storage_flags::StorageFlags; + + const PLAIN_CONTRACT: &str = "tests/supporting_files/contract/family/family-contract.json"; + const HISTORY_CONTRACT: &str = + "tests/supporting_files/contract/references/references_with_contract_history.json"; + + /// Three contracts with ids `[n; 32]`: two plain, one keeping history. + const LAYOUTS: &[(u8, bool)] = &[(1, false), (2, true), (3, false)]; + + fn block(time_ms: u64) -> BlockInfo { + BlockInfo { + time_ms, + height: 100, + core_height: 10, + epoch: Default::default(), + } + } + + fn apply(drive: &Drive, contract: &DataContract, time_ms: u64) { + drive + .apply_contract( + contract, + block(time_ms), + true, + StorageFlags::optional_default_as_cow(), + None, + PlatformVersion::latest(), + ) + .expect("expected to apply the contract"); + } + + fn setup_contracts() -> (Drive, Vec) { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let contracts = LAYOUTS + .iter() + .map(|(seed, keeps_history)| { + let fixture = if *keeps_history { + HISTORY_CONTRACT + } else { + PLAIN_CONTRACT + }; + let mut contract = json_document_to_contract(fixture, false, platform_version) + .expect("expected to load the contract fixture"); + contract.set_id([*seed; 32].into()); + if *keeps_history { + contract.config_mut().set_keeps_history(true); + contract.config_mut().set_readonly(false); + } + apply(&drive, &contract, 1000); + contract + }) + .collect(); + (drive, contracts) + } + + fn prove_and_verify(drive: &Drive, ids: &[[u8; 32]]) -> super::DataContractsVersions { + let platform_version = PlatformVersion::latest(); + let proof = drive + .prove_contracts_versions(ids, None, platform_version) + .expect("expected to prove the versions"); + let (_root_hash, versions) = + Drive::verify_contracts_versions(&proof, ids, platform_version) + .expect("expected to verify the versions"); + versions + } + + #[test] + fn should_prove_and_verify_versions_of_both_layouts_and_absence() { + let (drive, contracts) = setup_contracts(); + let missing = [9; 32]; + let ids: Vec<[u8; 32]> = contracts + .iter() + .map(|contract| contract.id().to_buffer()) + .chain([missing]) + .collect(); + + let versions = prove_and_verify(&drive, &ids); + + assert_eq!(versions.len(), ids.len()); + for contract in &contracts { + assert_eq!( + versions.get(&contract.id().to_buffer()), + Some(&Some(contract.version())), + "every stored contract proves its version" + ); + } + assert_eq!( + versions.get(&missing), + Some(&None), + "a missing id proves absent" + ); + } + + #[test] + fn should_prove_the_version_after_an_update_and_fold_duplicate_ids() { + let (drive, mut contracts) = setup_contracts(); + let contract = &mut contracts[1]; + contract.increment_version(); + apply(&drive, contract, 2000); + let id = contract.id().to_buffer(); + + let versions = prove_and_verify(&drive, &[id, id]); + + assert_eq!(versions.len(), 1); + assert_eq!(versions.get(&id), Some(&Some(contract.version()))); + } + + #[test] + fn should_reject_a_proof_verified_against_different_ids() { + let (drive, contracts) = setup_contracts(); + let platform_version = PlatformVersion::latest(); + let proved_ids = [contracts[0].id().to_buffer()]; + let proof = drive + .prove_contracts_versions(&proved_ids, None, platform_version) + .expect("expected to prove the versions"); + + let other_ids = [contracts[1].id().to_buffer()]; + assert!( + Drive::verify_contracts_versions(&proof, &other_ids, platform_version).is_err(), + "a proof of one id must not verify for another" + ); + + let more_ids = [contracts[0].id().to_buffer(), contracts[1].id().to_buffer()]; + assert!( + Drive::verify_contracts_versions(&proof, &more_ids, platform_version).is_err(), + "a proof of one id must not verify for two" + ); + } + + #[test] + fn should_reject_a_tampered_proof() { + let (drive, contracts) = setup_contracts(); + let platform_version = PlatformVersion::latest(); + let ids: Vec<[u8; 32]> = contracts.iter().map(|c| c.id().to_buffer()).collect(); + let mut proof = drive + .prove_contracts_versions(&ids, None, platform_version) + .expect("expected to prove the versions"); + let middle = proof.len() / 2; + proof[middle] ^= 0xff; + + assert!(matches!( + Drive::verify_contracts_versions(&proof, &ids, platform_version), + Err(Error::GroveDB(_)) | Err(Error::Proof(_)) + )); + } + + #[test] + fn should_reject_empty_ids() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + assert!(drive + .prove_contracts_versions(&[], None, platform_version) + .is_err()); + assert!(Drive::verify_contracts_versions(&[], &[], platform_version).is_err()); + } + + #[test] + fn should_prove_the_versions_far_smaller_than_the_contracts() { + let (drive, contracts) = setup_contracts(); + let platform_version = PlatformVersion::latest(); + let ids: Vec<[u8; 32]> = contracts.iter().map(|c| c.id().to_buffer()).collect(); + + let versions_proof = drive + .prove_contracts_versions(&ids, None, platform_version) + .expect("expected to prove the versions"); + let contracts_proof = drive + .prove_contracts(&ids, None, platform_version) + .expect("expected to prove the contracts"); + + assert!( + versions_proof.len() * 4 < contracts_proof.len(), + "the version proof ({} bytes) must be well under a quarter of the contract proof ({} bytes)", + versions_proof.len(), + contracts_proof.len() + ); + } +} diff --git a/packages/rs-drive/tests/deterministic_root_hash.rs b/packages/rs-drive/tests/deterministic_root_hash.rs index 63b362aecf1..20cd3437577 100644 --- a/packages/rs-drive/tests/deterministic_root_hash.rs +++ b/packages/rs-drive/tests/deterministic_root_hash.rs @@ -306,7 +306,10 @@ mod tests { // payment address fields, changing the contract's serialized bytes // and therefore the root hash after insertion. 9..=13 => "14d9e2cdc3f25d1dfd079c1f9dd0d44db5bf73d397b04258449231a2d5bafda7", - _ => "02a78b45ecac17a6c08ce352c22b888fcd044de9f4ff82983c5d8fd97e6e8793", + // Protocol version 14 also stores the contract's version as a four-byte + // item beside the contract (`[64, id] / 2`), one more element under the + // contract's root subtree. + _ => "c5f12fcb423d17a25ea7969d3d51ce83f48a796392b7ceaf195f162647ec8206", }; assert_eq!( diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs index 97c155911df..4e00a02840d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs @@ -20,6 +20,11 @@ pub struct DriveAbciQueryVersions { /// future protocol version may need to make without re-cutting /// the wire shape. pub document_query_helpers: DriveAbciDocumentQueryHelperVersions, + /// Per-helper version slots for the data contract queries. Separate from + /// `data_contract_based_queries` (which versions the wire surface) because + /// the helper output is consensus-relevant on the query path: it selects + /// which state elements a query reads and proves. + pub data_contract_query_helpers: DriveAbciDataContractQueryHelperVersions, pub prefunded_specialized_balances: DriveAbciQueryPrefundedSpecializedBalancesVersions, pub identity_based_queries: DriveAbciQueryIdentityVersions, pub token_queries: DriveAbciQueryTokenVersions, @@ -41,6 +46,21 @@ pub struct DriveAbciDocumentQueryHelperVersions { pub compute_aggregate_mode_and_check_limit: FeatureVersion, } +#[derive(Clone, Debug, Default)] +pub struct DriveAbciDataContractQueryHelperVersions { + /// Version of the helper that answers `getDataContractsLatestVersions` + /// when the contracts themselves are not requested. + /// + /// * `0`: state holds no contract version items (before protocol + /// version 14); the answer is read from the serialized contracts and + /// the proof is the multi-contract proof, so it carries the contracts. + /// * `1`: every contract has a version item beside it (`[64, id] / 2`, + /// written from protocol version 14 and backfilled on its first block); + /// the answer is read from the item and the proof carries the items, + /// four bytes per contract, instead of the contracts. + pub latest_versions_read: FeatureVersion, +} + #[derive(Clone, Debug, Default)] pub struct DriveAbciQueryPrefundedSpecializedBalancesVersions { pub balance: FeatureVersionBounds, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs index 8e9b7bd3192..4462a3cf293 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs @@ -1,9 +1,10 @@ use crate::version::drive_abci_versions::drive_abci_query_versions::{ - DriveAbciDocumentQueryHelperVersions, DriveAbciQueryAddressFundsVersions, - DriveAbciQueryDataContractVersions, DriveAbciQueryGroupVersions, - DriveAbciQueryIdentityVersions, DriveAbciQueryPrefundedSpecializedBalancesVersions, - DriveAbciQueryShieldedVersions, DriveAbciQuerySystemVersions, DriveAbciQueryTokenVersions, - DriveAbciQueryValidatorVersions, DriveAbciQueryVersions, DriveAbciQueryVotingVersions, + DriveAbciDataContractQueryHelperVersions, DriveAbciDocumentQueryHelperVersions, + DriveAbciQueryAddressFundsVersions, DriveAbciQueryDataContractVersions, + DriveAbciQueryGroupVersions, DriveAbciQueryIdentityVersions, + DriveAbciQueryPrefundedSpecializedBalancesVersions, DriveAbciQueryShieldedVersions, + DriveAbciQuerySystemVersions, DriveAbciQueryTokenVersions, DriveAbciQueryValidatorVersions, + DriveAbciQueryVersions, DriveAbciQueryVotingVersions, }; use versioned_feature_core::FeatureVersionBounds; @@ -30,6 +31,9 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V0: DriveAbciQueryVersions = DriveAbciQueryV document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, + data_contract_query_helpers: DriveAbciDataContractQueryHelperVersions { + latest_versions_read: 0, + }, prefunded_specialized_balances: DriveAbciQueryPrefundedSpecializedBalancesVersions { balance: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs index 524e3706034..1ab0fc4fae2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs @@ -1,9 +1,10 @@ use crate::version::drive_abci_versions::drive_abci_query_versions::{ - DriveAbciDocumentQueryHelperVersions, DriveAbciQueryAddressFundsVersions, - DriveAbciQueryDataContractVersions, DriveAbciQueryGroupVersions, - DriveAbciQueryIdentityVersions, DriveAbciQueryPrefundedSpecializedBalancesVersions, - DriveAbciQueryShieldedVersions, DriveAbciQuerySystemVersions, DriveAbciQueryTokenVersions, - DriveAbciQueryValidatorVersions, DriveAbciQueryVersions, DriveAbciQueryVotingVersions, + DriveAbciDataContractQueryHelperVersions, DriveAbciDocumentQueryHelperVersions, + DriveAbciQueryAddressFundsVersions, DriveAbciQueryDataContractVersions, + DriveAbciQueryGroupVersions, DriveAbciQueryIdentityVersions, + DriveAbciQueryPrefundedSpecializedBalancesVersions, DriveAbciQueryShieldedVersions, + DriveAbciQuerySystemVersions, DriveAbciQueryTokenVersions, DriveAbciQueryValidatorVersions, + DriveAbciQueryVersions, DriveAbciQueryVotingVersions, }; use versioned_feature_core::FeatureVersionBounds; @@ -32,6 +33,9 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V1: DriveAbciQueryVersions = DriveAbciQueryV document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, + data_contract_query_helpers: DriveAbciDataContractQueryHelperVersions { + latest_versions_read: 0, + }, prefunded_specialized_balances: DriveAbciQueryPrefundedSpecializedBalancesVersions { balance: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs index 6b0098a97c4..68cd9426c9c 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs @@ -1,11 +1,21 @@ use crate::version::drive_abci_versions::drive_abci_query_versions::v2::DRIVE_ABCI_QUERY_VERSIONS_V2; use crate::version::drive_abci_versions::drive_abci_query_versions::{ - DriveAbciDocumentQueryHelperVersions, DriveAbciQueryVersions, + DriveAbciDataContractQueryHelperVersions, DriveAbciDocumentQueryHelperVersions, + DriveAbciQueryVersions, }; /// Version 3 of the Drive ABCI query versions. /// -/// Differs from v2 in exactly one slot: +/// Differs from v2 in two slots. +/// +/// `data_contract_query_helpers.latest_versions_read` is 1 rather than 0: +/// from protocol version 14 every contract carries a four-byte version item +/// beside it (`[64, id] / 2`, backfilled on the first block of the version), +/// so `getDataContractsLatestVersions` without `include_contracts` reads that +/// item and proves it instead of the contracts. The tables protocol versions +/// 1 to 13 select keep helper version 0, which reads and proves the contracts +/// a state without the items still has. +/// /// `document_query_helpers.compute_aggregate_mode_and_check_limit` is 2 /// rather than 1. That is the boolean-`HAVING` routing gate. The v1 /// helper rejects every non-empty `having` ("HAVING clause is not yet @@ -29,5 +39,8 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V3: DriveAbciQueryVersions = DriveAbciQueryV document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 2, }, + data_contract_query_helpers: DriveAbciDataContractQueryHelperVersions { + latest_versions_read: 1, + }, ..DRIVE_ABCI_QUERY_VERSIONS_V2 }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs index 8a317570ca3..88037c40214 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs @@ -3,6 +3,7 @@ use versioned_feature_core::FeatureVersion; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct DriveContractMethodVersions { @@ -20,6 +21,7 @@ pub struct DriveContractProveMethodVersions { pub prove_contract_history: FeatureVersion, pub prove_contracts: FeatureVersion, pub prove_contracts_by_range: FeatureVersion, + pub prove_contracts_versions: FeatureVersion, } #[derive(Clone, Debug, Default)] @@ -47,6 +49,7 @@ pub struct DriveContractUpdateMethodVersions { pub struct DriveContractGetMethodVersions { pub fetch_contract: FeatureVersion, pub fetch_contract_ids: FeatureVersion, + pub fetch_contract_version: FeatureVersion, pub fetch_contracts: FeatureVersion, pub fetch_contract_with_history: FeatureVersion, pub get_cached_contract_with_fetch_info: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v1.rs index 801708207f5..f2d330d5fcc 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v1.rs @@ -11,6 +11,7 @@ pub const DRIVE_CONTRACT_METHOD_VERSIONS_V1: DriveContractMethodVersions = prove_contract_history: 0, prove_contracts: 0, prove_contracts_by_range: 0, + prove_contracts_versions: 0, }, apply: DriveContractApplyMethodVersions { apply_contract: 0, @@ -33,6 +34,7 @@ pub const DRIVE_CONTRACT_METHOD_VERSIONS_V1: DriveContractMethodVersions = get: DriveContractGetMethodVersions { fetch_contract: 0, fetch_contract_ids: 0, + fetch_contract_version: 0, fetch_contracts: 0, fetch_contract_with_history: 0, get_cached_contract_with_fetch_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v2.rs index acccd1f2d42..dfa424d5df3 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v2.rs @@ -11,6 +11,7 @@ pub const DRIVE_CONTRACT_METHOD_VERSIONS_V2: DriveContractMethodVersions = prove_contract_history: 0, prove_contracts: 0, prove_contracts_by_range: 0, + prove_contracts_versions: 0, }, apply: DriveContractApplyMethodVersions { apply_contract: 0, @@ -33,6 +34,7 @@ pub const DRIVE_CONTRACT_METHOD_VERSIONS_V2: DriveContractMethodVersions = get: DriveContractGetMethodVersions { fetch_contract: 0, fetch_contract_ids: 0, + fetch_contract_version: 0, fetch_contracts: 0, fetch_contract_with_history: 0, get_cached_contract_with_fetch_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v3.rs index 5d0b4f1d5c2..e4e46eab398 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v3.rs @@ -22,6 +22,7 @@ pub const DRIVE_CONTRACT_METHOD_VERSIONS_V3: DriveContractMethodVersions = prove_contract_history: 0, prove_contracts: 0, prove_contracts_by_range: 0, + prove_contracts_versions: 0, }, apply: DriveContractApplyMethodVersions { apply_contract: 0, @@ -44,6 +45,7 @@ pub const DRIVE_CONTRACT_METHOD_VERSIONS_V3: DriveContractMethodVersions = get: DriveContractGetMethodVersions { fetch_contract: 0, fetch_contract_ids: 0, + fetch_contract_version: 0, fetch_contracts: 0, fetch_contract_with_history: 0, get_cached_contract_with_fetch_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs new file mode 100644 index 00000000000..cb41f12e04c --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs @@ -0,0 +1,24 @@ +use crate::version::drive_versions::drive_contract_method_versions::v3::DRIVE_CONTRACT_METHOD_VERSIONS_V3; +use crate::version::drive_versions::drive_contract_method_versions::{ + DriveContractInsertMethodVersions, DriveContractMethodVersions, +}; + +/// Drive contract methods for protocol v14+. +/// +/// Identical to [`super::v3::DRIVE_CONTRACT_METHOD_VERSIONS_V3`] except +/// `insert.add_contract_to_storage` is bumped to `1`. +/// +/// The v1 storage writer stores, beside the contract, a four-byte item holding +/// the contract's version number (`[64, id] / 2`) on every contract create and +/// update. That item is what lets `getDataContractsLatestVersions` read and +/// prove a contract's version without the contract bytes. Contracts stored +/// before this version get their item on the first block of protocol version +/// 14 (`Drive::add_version_items_to_all_contracts`). +pub const DRIVE_CONTRACT_METHOD_VERSIONS_V4: DriveContractMethodVersions = + DriveContractMethodVersions { + insert: DriveContractInsertMethodVersions { + add_contract_to_storage: 1, + ..DRIVE_CONTRACT_METHOD_VERSIONS_V3.insert + }, + ..DRIVE_CONTRACT_METHOD_VERSIONS_V3 + }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs index d73315cb310..f9c0adc8fe8 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs @@ -39,6 +39,7 @@ pub struct DriveVerifyContractMethodVersions { pub verify_contract_history: FeatureVersion, pub verify_contract_return_serialization: FeatureVersion, pub verify_contracts_by_range: FeatureVersion, + pub verify_contracts_versions: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs index 039b18fe759..8c00b291fe5 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs @@ -15,6 +15,7 @@ pub const DRIVE_VERIFY_METHOD_VERSIONS_V1: DriveVerifyMethodVersions = DriveVeri verify_contract_history: 0, verify_contract_return_serialization: 0, verify_contracts_by_range: 0, + verify_contracts_versions: 0, }, document: DriveVerifyDocumentMethodVersions { verify_proof: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index fade08c521d..4f5e98c565a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -1,5 +1,5 @@ use crate::version::drive_versions::drive_address_funds_method_versions::v2::DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_contract_method_versions::v3::DRIVE_CONTRACT_METHOD_VERSIONS_V3; +use crate::version::drive_versions::drive_contract_method_versions::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; use crate::version::drive_versions::drive_credit_pool_method_versions::v1::CREDIT_POOL_METHOD_VERSIONS_V1; use crate::version::drive_versions::drive_document_method_versions::v4::DRIVE_DOCUMENT_METHOD_VERSIONS_V4; use crate::version::drive_versions::drive_group_method_versions::v1::DRIVE_GROUP_METHOD_VERSIONS_V1; @@ -68,7 +68,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { }, document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot vote: DRIVE_VOTE_METHOD_VERSIONS_V2, - contract: DRIVE_CONTRACT_METHOD_VERSIONS_V3, // changed in v8: count-tree-aware contract-insertion cost estimation (v12+ countable/range_countable doctypes) + contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract fees: DriveFeesMethodVersions { calculate_fee: 0 }, estimated_costs: DriveEstimatedCostsMethodVersions { add_estimation_costs_for_levels_up_to_contract: 0, 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 a41e7c5fc8e..49e0cb4048f 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -17,11 +17,12 @@ use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1; use crate::version::drive_abci_versions::drive_abci_method_versions::v1::DRIVE_ABCI_METHOD_VERSIONS_V1; use crate::version::drive_abci_versions::drive_abci_query_versions::{ - DriveAbciDocumentQueryHelperVersions, DriveAbciQueryAddressFundsVersions, - DriveAbciQueryDataContractVersions, DriveAbciQueryGroupVersions, - DriveAbciQueryIdentityVersions, DriveAbciQueryPrefundedSpecializedBalancesVersions, - DriveAbciQueryShieldedVersions, DriveAbciQuerySystemVersions, DriveAbciQueryTokenVersions, - DriveAbciQueryValidatorVersions, DriveAbciQueryVersions, DriveAbciQueryVotingVersions, + DriveAbciDataContractQueryHelperVersions, DriveAbciDocumentQueryHelperVersions, + DriveAbciQueryAddressFundsVersions, DriveAbciQueryDataContractVersions, + DriveAbciQueryGroupVersions, DriveAbciQueryIdentityVersions, + DriveAbciQueryPrefundedSpecializedBalancesVersions, DriveAbciQueryShieldedVersions, + DriveAbciQuerySystemVersions, DriveAbciQueryTokenVersions, DriveAbciQueryValidatorVersions, + DriveAbciQueryVersions, DriveAbciQueryVotingVersions, }; use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1; use crate::version::drive_abci_versions::drive_abci_validation_versions::v1::DRIVE_ABCI_VALIDATION_VERSIONS_V1; @@ -180,6 +181,9 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, + data_contract_query_helpers: DriveAbciDataContractQueryHelperVersions { + latest_versions_read: 0, + }, prefunded_specialized_balances: DriveAbciQueryPrefundedSpecializedBalancesVersions { balance: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-sdk/src/platform/data_contracts_latest_versions.rs b/packages/rs-sdk/src/platform/data_contracts_latest_versions.rs index 175df1433e3..5374c44e9cd 100644 --- a/packages/rs-sdk/src/platform/data_contracts_latest_versions.rs +++ b/packages/rs-sdk/src/platform/data_contracts_latest_versions.rs @@ -1,12 +1,15 @@ //! The current versions of data contracts (`getDataContractsLatestVersions`). //! -//! The cheap way to check that contracts held locally are still current. The unproved fetch -//! ([`FetchUnproved::fetch_unproved`] on [`DataContractsLatestVersions`]) is answered from -//! Drive's contract cache with one integer per contract. The proved fetch -//! ([`FetchMany::fetch_many`] on [`DataContractLatestVersion`]) verifies the same -//! multi-contract proof `getDataContracts` returns, so it costs as much as fetching the -//! contracts; use it when the answer has to be trusted. Either way the contracts themselves -//! come back only when [`DataContractsLatestVersionsQuery::include_contracts`] is set. +//! The cheap way to check that contracts held locally are still current. From protocol +//! version 14 every contract carries a four-byte version item in state, and a query without +//! [`DataContractsLatestVersionsQuery::include_contracts`] is answered from it: the unproved +//! fetch ([`FetchUnproved::fetch_unproved`] on [`DataContractsLatestVersions`]) returns one +//! integer per contract, and the proved fetch ([`FetchMany::fetch_many`] on +//! [`DataContractLatestVersion`]) verifies a proof of the items, a few hundred bytes of hash +//! path per contract. With `include_contracts`, and on earlier protocol versions, the proof +//! is the multi-contract proof `getDataContracts` returns, so it costs as much as fetching +//! the contracts. Either way the contracts themselves come back only when +//! `include_contracts` is set. use crate::platform::{FetchMany, FetchUnproved, Identifier, Query, QuerySettings}; use crate::Error; diff --git a/packages/wasm-sdk/src/queries/data_contract.rs b/packages/wasm-sdk/src/queries/data_contract.rs index 1c91597f18e..dc9bcb7b6bf 100644 --- a/packages/wasm-sdk/src/queries/data_contract.rs +++ b/packages/wasm-sdk/src/queries/data_contract.rs @@ -462,8 +462,11 @@ impl WasmSdk { /// are still current. One map entry per requested id, `undefined` for an id no contract /// has; contracts come back only with `includeContracts`. /// - /// This SDK verifies the answer, and the proof is the multi-contract proof, so the call - /// costs as much as `getDataContracts`; the saving is on the unproved gRPC path. + /// This SDK verifies the answer. From protocol version 14, without `includeContracts`, + /// the proof covers the four-byte version item each contract carries in state, a few + /// hundred bytes of hash path per contract. With `includeContracts`, and on earlier + /// protocol versions, the proof is the multi-contract proof, so the call costs as much + /// as `getDataContracts`. #[wasm_bindgen( js_name = "getDataContractsLatestVersions", unchecked_return_type = "Map" From 4cb3c5e6055619139bb612edba9f6d0a9191f41e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 00:46:45 +0700 Subject: [PATCH 02/10] test(drive): re-pin document insert and contract check_tx fees for the PV14 version item From protocol version 14 a contract's root subtree holds one more element, the version item, so every document insert rehashes one more node (+740 credits of processing) and a contract create or update writes the item (+61200 credits). The tests that pin those fees at the latest protocol version move accordingly, and each moved baseline gets a protocol version 13 twin that stores the contract without the item and keeps the previous value, proving the change is gated to the unreleased protocol version. Co-Authored-By: Claude Fable 5.1 --- .../src/execution/check_tx/v0/mod.rs | 380 +++++++++++++++++- .../rs-drive/src/drive/document/insert/mod.rs | 123 +++++- 2 files changed, 478 insertions(+), 25 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index c32b8ebd7a4..bdd3734e17a 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -688,7 +688,157 @@ mod tests { assert_eq!( processing_result.aggregated_fees().processing_fee, - 24002816630 + // from protocol version 14 the contract's version item is stored beside the contract + 24002877830 + ); + + let check_result = platform + .check_tx( + serialized.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(check_result.is_valid()); // it should still be valid, because we didn't commit the transaction + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + let check_result = platform + .check_tx( + serialized.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(!check_result.is_valid()); // it should no longer be valid, because of the nonce check + + assert!(matches!( + check_result.errors.first().expect("expected an error"), + ConsensusError::StateError(StateError::InvalidIdentityNonceError(_)) + )); + } + + /// Protocol version 13 stores no contract version item, so the create fee stays where it + /// was before protocol version 14 added the item. + #[test] + fn data_contract_create_check_tx_protocol_version_13() { + let platform_config = PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }; + + let platform = TestPlatformBuilder::new() + .with_config(platform_config) + .with_initial_protocol_version(13) + .build_with_mock_rpc(); + + let platform_state = platform.state.load(); + let protocol_version = platform_state.current_protocol_version_in_consensus(); + let platform_version = PlatformVersion::get(protocol_version).unwrap(); + + let (key, private_key) = IdentityPublicKey::random_ecdsa_critical_level_authentication_key( + 1, + Some(1), + platform_version, + ) + .expect("expected to get key pair"); + + platform + .drive + .create_initial_state_structure(None, platform_version) + .expect("expected to create state structure"); + let identity: Identity = IdentityV0 { + id: Identifier::new([ + 158, 113, 180, 126, 91, 83, 62, 44, 83, 54, 97, 88, 240, 215, 84, 139, 167, 156, + 166, 203, 222, 4, 64, 31, 215, 199, 149, 151, 190, 246, 251, 44, + ]), + public_keys: BTreeMap::from([(1, key.clone())]), + balance: 25_000_000_000, // 0.25 Dash + revision: 0, + } + .into(); + + let dashpay = get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); + let mut create_contract_state_transition: StateTransition = dashpay + .try_into_platform_versioned(platform_version) + .expect("expected a state transition"); + create_contract_state_transition + .sign(&key, private_key.as_slice(), &NativeBlsModule) + .expect("expected to sign transition"); + let serialized = create_contract_state_transition + .serialize_to_bytes() + .expect("serialized state transition"); + platform + .drive + .add_new_identity( + identity, + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to insert identity"); + + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &platform_state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + + let validation_result = platform + .check_tx( + serialized.as_slice(), + FirstTimeCheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert_eq!(validation_result.errors.as_slice(), &[]); + + let check_result = platform + .check_tx( + serialized.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(check_result.is_valid()); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + std::slice::from_ref(&serialized), + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_eq!( + processing_result.aggregated_fees().processing_fee, + 24002489210 ); let check_result = platform @@ -1203,7 +1353,8 @@ mod tests { // Plus we have 24_000_000_000 in base costs assert_eq!( processing_result.aggregated_fees().processing_fee, - 24005633260 + // from protocol version 14 the contract's version item is stored beside the contract + 24005755660 ); let check_result = platform @@ -1678,7 +1829,225 @@ mod tests { assert_eq!( processing_result.aggregated_fees().processing_fee, - 24002816630 + // from protocol version 14 the contract's version item is stored beside the contract + 24002877830 + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + // Now let's do the data contract update + let _dashpay_id = modified_dashpay_contract.id(); + // we need to alter dashpay to make it invalid + + modified_dashpay_contract.set_version(2); + + let document_types = modified_dashpay_contract.document_types_mut(); + + let dpns_contract = + get_dpns_data_contract_fixture(Some(identity.id()), 1, protocol_version) + .data_contract_owned(); + + document_types.insert( + "preorder".to_string(), + dpns_contract + .document_type_for_name("preorder") + .expect("expected document type") + .to_owned_document_type(), + ); + + let mut update_contract_state_transition: StateTransition = + DataContractUpdateTransition::try_from_platform_versioned( + (modified_dashpay_contract, 2), + platform_version, + ) + .expect("expected a state transition") + .into(); + + update_contract_state_transition + .sign(&key, private_key.as_slice(), &NativeBlsModule) + .expect("expected to sign transition"); + let serialized_update = update_contract_state_transition + .serialize_to_bytes() + .expect("serialized state transition"); + + let validation_result = platform + .check_tx( + serialized_update.as_slice(), + FirstTimeCheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert_eq!(validation_result.errors.as_slice(), &[]); + + let check_result = platform + .check_tx( + serialized_update.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(check_result.is_valid()); + + let transaction = platform.drive.grove.start_transaction(); + + let update_processing_result = platform + .platform + .process_raw_state_transitions( + std::slice::from_ref(&serialized_update), + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // We have one invalid paid for state transition + assert_eq!(update_processing_result.valid_count(), 1); + + assert_eq!( + update_processing_result.aggregated_fees().processing_fee, + // from protocol version 14 the contract's version item is stored beside the contract + 27002932650 + ); + + let check_result = platform + .check_tx( + serialized_update.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(check_result.is_valid()); // it should still be valid, because we didn't commit the transaction + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + let check_result = platform + .check_tx( + serialized_update.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(!check_result.is_valid()); // it should no longer be valid, because of the nonce check + + assert!(matches!( + check_result.errors.first().expect("expected an error"), + ConsensusError::StateError(StateError::InvalidIdentityNonceError(_)) + )); + } + + /// Protocol version 13 stores no contract version item, so the update fee stays where it + /// was before protocol version 14 added the item. + #[test] + fn data_contract_update_check_tx_protocol_version_13() { + let platform_config = PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }; + + let platform = TestPlatformBuilder::new() + .with_config(platform_config) + .with_initial_protocol_version(13) + .build_with_mock_rpc(); + + let platform_state = platform.state.load(); + let protocol_version = platform_state.current_protocol_version_in_consensus(); + let platform_version = PlatformVersion::get(protocol_version).unwrap(); + + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &platform_state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + + let (key, private_key) = IdentityPublicKey::random_ecdsa_critical_level_authentication_key( + 1, + Some(1), + platform_version, + ) + .expect("expected to get key pair"); + + platform + .drive + .create_initial_state_structure(None, platform_version) + .expect("expected to create state structure"); + let identity: Identity = IdentityV0 { + id: Identifier::new([ + 158, 113, 180, 126, 91, 83, 62, 44, 83, 54, 97, 88, 240, 215, 84, 139, 167, 156, + 166, 203, 222, 4, 64, 31, 215, 199, 149, 151, 190, 246, 251, 44, + ]), + public_keys: BTreeMap::from([(1, key.clone())]), + balance: 100_000_000_000, // 1.0 Dash + revision: 0, + } + .into(); + + let dashpay_created_contract = + get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); + let mut modified_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let mut create_contract_state_transition: StateTransition = dashpay_created_contract + .try_into_platform_versioned(platform_version) + .expect("expected a state transition"); + create_contract_state_transition + .sign(&key, private_key.as_slice(), &NativeBlsModule) + .expect("expected to sign transition"); + let serialized = create_contract_state_transition + .serialize_to_bytes() + .expect("serialized state transition"); + platform + .drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to insert identity"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + std::slice::from_ref(&serialized), + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_eq!( + processing_result.aggregated_fees().processing_fee, + 24002489210 ); platform @@ -1765,7 +2134,7 @@ mod tests { assert_eq!( update_processing_result.aggregated_fees().processing_fee, - 27002879350 + 27002504030 ); let check_result = platform @@ -2137,7 +2506,8 @@ mod tests { assert_eq!( processing_result.aggregated_fees().processing_fee, - 24002816630 + // from protocol version 14 the contract's version item is stored beside the contract + 24002877830 ); platform diff --git a/packages/rs-drive/src/drive/document/insert/mod.rs b/packages/rs-drive/src/drive/document/insert/mod.rs index 6ca38b190e9..c70972cda78 100644 --- a/packages/rs-drive/src/drive/document/insert/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/mod.rs @@ -335,14 +335,16 @@ mod tests { .expect("expected to override a document successfully"); } - #[test] - fn test_add_dashpay_contact_request_with_fee() { + /// Inserts a dashpay contact request under a contract stored with `platform_version` + /// and asserts the exact fee. + fn do_test_add_dashpay_contact_request_with_fee( + platform_version: &PlatformVersion, + expected_processing_fee: u64, + ) { let drive = setup_drive_with_initial_state_structure(None); let db_transaction = drive.grove.start_transaction(); - let platform_version = PlatformVersion::latest(); - let contract = setup_contract( &drive, "tests/supporting_files/contract/dashpay/dashpay-contract.json", @@ -350,7 +352,7 @@ mod tests { None, None::, Some(&db_transaction), - None, + Some(platform_version), ); let document_type = contract @@ -397,12 +399,26 @@ mod tests { &EPOCH_CHANGE_FEE_VERSION_TEST, StorageDiskUsageCreditPerByte, ), - processing_fee: 1695100, + processing_fee: expected_processing_fee, ..Default::default() } ); } + #[test] + fn test_add_dashpay_contact_request_with_fee() { + // From protocol version 14 the contract's root subtree also holds the contract's + // version item, one more node to rehash on every document insert. + do_test_add_dashpay_contact_request_with_fee(PlatformVersion::latest(), 1695840); + } + + #[test] + fn test_add_dashpay_contact_request_with_fee_protocol_version_13() { + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + + do_test_add_dashpay_contact_request_with_fee(platform_version, 1695100); + } + #[test] fn test_add_dashpay_profile_with_fee_first_version_apply() { let platform_version = PlatformVersion::first(); @@ -412,11 +428,18 @@ mod tests { &EPOCH_CHANGE_FEE_VERSION_TEST, StorageDiskUsageCreditPerByte, ), - processing_fee: 900400, + // the contract stored under the latest protocol version carries its + // version item, one more node to rehash on the insert + processing_fee: 901140, ..Default::default() }; - do_test_add_dashpay_profile_with_fee(true, platform_version, expected_fee_result); + do_test_add_dashpay_profile_with_fee( + true, + PlatformVersion::latest(), + platform_version, + expected_fee_result, + ); } #[test] @@ -432,7 +455,12 @@ mod tests { ..Default::default() }; - do_test_add_dashpay_profile_with_fee(false, platform_version, expected_fee_result); + do_test_add_dashpay_profile_with_fee( + false, + PlatformVersion::latest(), + platform_version, + expected_fee_result, + ); } #[test] @@ -444,11 +472,18 @@ mod tests { &EPOCH_CHANGE_FEE_VERSION_TEST, StorageDiskUsageCreditPerByte, ), - processing_fee: 900400, + // the contract stored under the latest protocol version carries its + // version item, one more node to rehash on the insert + processing_fee: 901140, ..Default::default() }; - do_test_add_dashpay_profile_with_fee(true, platform_version, expected_fee_result); + do_test_add_dashpay_profile_with_fee( + true, + PlatformVersion::latest(), + platform_version, + expected_fee_result, + ); } #[test] @@ -466,7 +501,35 @@ mod tests { ..Default::default() }; - do_test_add_dashpay_profile_with_fee(false, platform_version, expected_fee_result); + do_test_add_dashpay_profile_with_fee( + false, + PlatformVersion::latest(), + platform_version, + expected_fee_result, + ); + } + + /// Before protocol version 14 the contract's root subtree has no version item, so the + /// insert rehashes one node fewer than under the latest version. + #[test] + fn test_add_dashpay_profile_with_fee_protocol_version_13_apply() { + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + let expected_fee_result = FeeResult { + storage_fee: 1305 + * Epoch::new(0).unwrap().cost_for_known_cost_item( + &EPOCH_CHANGE_FEE_VERSION_TEST, + StorageDiskUsageCreditPerByte, + ), + processing_fee: 900400, + ..Default::default() + }; + + do_test_add_dashpay_profile_with_fee( + true, + platform_version, + platform_version, + expected_fee_result, + ); } /// This helper sets up the environment, adds a dashpay profile document, @@ -474,10 +537,14 @@ mod tests { /// /// `apply`: if true, we commit the transaction (applying the changes). /// if false, we do not commit, so changes are only estimated. - /// `platform_version`: which PlatformVersion to use. + /// `contract_platform_version`: which PlatformVersion stores the contract; from + /// protocol version 14 that adds the contract's version item to its root + /// subtree, one more node to rehash on every document insert. + /// `platform_version`: which PlatformVersion to use for the document insert. /// `expected_fee_result`: the FeeResult we expect in the test assertion. fn do_test_add_dashpay_profile_with_fee( apply: bool, + contract_platform_version: &PlatformVersion, platform_version: &PlatformVersion, expected_fee_result: FeeResult, ) { @@ -493,7 +560,7 @@ mod tests { None, None::, Some(&db_transaction), - None, + Some(contract_platform_version), ); let document_type = contract @@ -711,14 +778,16 @@ mod tests { assert_eq!(actual_drive_operations.len(), fee_drive_operations.len()); } - #[test] - fn test_add_dpns_document_with_fee() { + /// Inserts a DPNS domain under a contract stored with `platform_version` and asserts + /// the exact fee. + fn do_test_add_dpns_document_with_fee( + platform_version: &PlatformVersion, + expected_processing_fee: u64, + ) { let drive = setup_drive_with_initial_state_structure(None); let db_transaction = drive.grove.start_transaction(); - let platform_version = PlatformVersion::latest(); - let contract = setup_contract( &drive, "tests/supporting_files/contract/dpns/dpns-contract.json", @@ -726,7 +795,7 @@ mod tests { None, None::, Some(&db_transaction), - None, + Some(platform_version), ); let random_owner_id = rand::thread_rng().gen::<[u8; 32]>(); @@ -772,7 +841,7 @@ mod tests { &EPOCH_CHANGE_FEE_VERSION_TEST, StorageDiskUsageCreditPerByte, ), - processing_fee: 1264300, + processing_fee: expected_processing_fee, ..Default::default() } ); @@ -784,6 +853,20 @@ mod tests { .expect("unable to commit transaction"); } + #[test] + fn test_add_dpns_document_with_fee() { + // From protocol version 14 the contract's root subtree also holds the contract's + // version item, one more node to rehash on every document insert. + do_test_add_dpns_document_with_fee(PlatformVersion::latest(), 1265040); + } + + #[test] + fn test_add_dpns_document_with_fee_protocol_version_13() { + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + + do_test_add_dpns_document_with_fee(platform_version, 1264300); + } + #[test] fn test_add_dashpay_many_non_conflicting_documents() { let (drive, dashpay) = setup_dashpay("add_no_conflict", true); From 40e1b54fa25e27f314ec93d98340e98b9f0fa69e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 01:00:04 +0700 Subject: [PATCH 03/10] test(drive-abci): re-pin latest-version document and token fees for the PV14 version item Every document write under a contract stored from protocol version 14 rehashes one more node of the contract's root subtree, the version item, so the latest-version baselines of the document delete, replace, transfer, NFT set price and purchase, DPNS reference, token burn group action and direct purchase tests move by 740 credits per write. The protocol version 11 and 13 twins keep their values; the DPNS twin helper now stores its contracts at the protocol version under test instead of the latest one, which is what made those twins move too. Co-Authored-By: Claude Fable 5.1 --- .../batch/tests/document/deletion.rs | 4 ++-- .../batch/tests/document/dpns.rs | 8 +++---- .../batch/tests/document/nft.rs | 24 +++++++++---------- .../batch/tests/document/replacement.rs | 2 +- .../batch/tests/document/transfer.rs | 10 ++++---- .../batch/tests/token/burn/mod.rs | 2 +- .../batch/tests/token/direct_selling/mod.rs | 2 +- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs index 27bcd8a748b..5d9165a286b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs @@ -12,7 +12,7 @@ mod deletion_tests { PlatformVersion::latest().protocol_version, // v14: the deleted document carries the contract-version stamp // (one stored byte, five estimated), shifting processing costs - 1699620, + 1700360, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -603,7 +603,7 @@ mod deletion_tests { async fn test_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted() { run_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted_at_protocol_version( PlatformVersion::latest().protocol_version, - 2778700, + 2779440, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } 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 ac0956c6916..bf7543ca371 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 @@ -25,7 +25,7 @@ mod dpns_tests { PlatformVersion::latest().protocol_version, // v14: GroveDB V4 writes through the Merk node it retains from // reading the old value, billing one fewer seek than the V3 path - 6_006_380, + 6_008_600, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -90,7 +90,7 @@ mod dpns_tests { None, None::, None, - None, + Some(platform_version), ); let card_game = setup_contract( @@ -100,7 +100,7 @@ mod dpns_tests { None, None::, None, - None, + Some(platform_version), ); let dpns_contract = setup_contract( @@ -110,7 +110,7 @@ mod dpns_tests { None, None::, None, - None, + Some(platform_version), ); let preorder = dpns_contract diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs index 902a665fdcf..d7449729e9a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs @@ -171,7 +171,7 @@ mod nft_tests { async fn test_document_set_price() { run_document_set_price_at_protocol_version( PlatformVersion::latest().protocol_version, - 2485600, + 2486340, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -409,9 +409,9 @@ mod nft_tests { async fn test_document_set_price_and_purchase() { run_document_set_price_and_purchase_at_protocol_version( PlatformVersion::latest().protocol_version, - 126440160, - 2485600, - 4092360, + 126440900, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 2486340, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 4093100, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -831,10 +831,10 @@ mod nft_tests { async fn test_document_set_price_and_purchase_different_epoch_documents_mutable() { run_document_set_price_and_purchase_different_epoch_documents_mutable_at_protocol_version( PlatformVersion::latest().protocol_version, - 141238960, - 2729120, - 2733160, - 4357440, + 141239700, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 2729860, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 2733900, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 4358180, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -1412,9 +1412,9 @@ mod nft_tests { async fn test_document_set_price_and_purchase_different_epoch() { run_document_set_price_and_purchase_different_epoch_at_protocol_version( PlatformVersion::latest().protocol_version, - 126440160, - 2485600, - 4092360, + 126440900, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 2486340, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash + 4093100, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -2614,7 +2614,7 @@ mod nft_tests { ) { run_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing_at_protocol_version( PlatformVersion::latest().protocol_version, - 2485600, + 2486340, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 3108d2d8eeb..0dae7a18c40 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -501,7 +501,7 @@ mod replacement_tests { // v14: replaced documents carry the contract-version stamp, and // GroveDB V4 writes through the Merk node it retains from reading // the old value, billing slightly fewer reads than the V3 path - 1428320, + 1429060, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs index e8f51ec6344..cbc96b3808f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs @@ -10,7 +10,7 @@ mod transfer_tests { ) { run_document_transfer_on_document_type_that_is_transferable_that_has_no_owner_indices_at_protocol_version( PlatformVersion::latest().protocol_version, - 1997120, + 1997860, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -213,7 +213,7 @@ mod transfer_tests { async fn test_document_transfer_on_document_type_that_is_transferable_before_creator_id() { run_document_transfer_on_document_type_that_is_transferable_before_creator_id_at_protocol_version( PlatformVersion::latest().protocol_version, - 3380960, + 3381700, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -499,7 +499,7 @@ mod transfer_tests { run_document_transfer_on_document_type_that_is_transferable_at_protocol_version( PlatformVersion::latest().protocol_version, // v14: transferred documents carry the contract-version stamp - 3643860, + 3644600, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -797,7 +797,7 @@ mod transfer_tests { async fn test_document_transfer_on_document_type_that_is_transferable_contract_v0() { run_document_transfer_on_document_type_that_is_transferable_contract_v0_at_protocol_version( PlatformVersion::latest().protocol_version, - 3380960, + 3381700, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } @@ -1491,7 +1491,7 @@ mod transfer_tests { async fn test_document_delete_after_transfer() { run_document_delete_after_transfer_at_protocol_version( PlatformVersion::latest().protocol_version, - 4004720, // v14: stamped documents (see transferable baseline note) + 4005460, // v14: stamped documents (see transferable baseline note) ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs index 32d40c65338..1bd1675c507 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs @@ -3960,7 +3960,7 @@ mod token_burn_tests { PlatformVersion::latest().protocol_version, // PROTOCOL_VERSION_14: +400 — genesis system documents now carry // the contract-version stamp, shifting byte-billed subtree reads - 4_368_280, + 4_369_020, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs index 11451ddcbae..f20e9b5abf9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs @@ -27,7 +27,7 @@ mod token_selling_tests { // PROTOCOL_VERSION_14: 27_400 credits more in fees — genesis system // documents now carry the contract-version stamp, shifting // byte-billed subtree reads - 699_868_046_180, + 699_868_045_440, // +740 per document write from protocol version 14: the contract's version item is one more node to rehash ) .await; } From 9a43f0c64a85e84b319e5f43da76d8c54e2c846d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 01:07:08 +0700 Subject: [PATCH 04/10] fix(drive): reject more contract ids than a version proof query can limit Review follow-ups: the docs say every distinct requested id gets an entry, since duplicate ids fold; and the version item prover and verifier reject more than 65,535 distinct ids instead of letting the query limit truncate, so the saturating conversion in the shared query builder is never reached. Co-Authored-By: Claude Fable 5.1 --- .../protos/platform/v0/platform.proto | 2 +- .../data_contracts_latest_versions/v0/mod.rs | 4 +-- .../types/data_contracts_latest_versions.rs | 2 +- .../prove/prove_contracts_versions/v0/mod.rs | 7 ++++++ .../rs-drive/src/drive/contract/queries.rs | 10 ++++++-- .../verify_contracts_versions/v0/mod.rs | 25 +++++++++++++++++++ 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 8066b4b555e..cba592445c4 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -503,7 +503,7 @@ message GetDataContractResponse { // Returns the current version number of each requested data contract: the // cheap way for a client to check that the contracts it already holds are -// still current. Every requested id gets an entry; an id no contract has gets +// still current. Every distinct requested id gets an entry; an id no contract has gets // an entry without `version`. Serialized contracts are added only when // `include_contracts` is set. // diff --git a/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs b/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs index 8b03b262dc8..d35c5c10b4e 100644 --- a/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contracts_latest_versions/v0/mod.rs @@ -22,8 +22,8 @@ use std::collections::BTreeSet; impl Platform { /// Returns the current version number of each requested data contract, and the serialized - /// contracts only when `include_contracts` is set. Every requested id gets an entry; an id - /// no contract has gets an entry without a version. + /// contracts only when `include_contracts` is set. Every distinct requested id gets an entry; + /// an id no contract has gets an entry without a version. /// /// From protocol version 14 every contract carries a four-byte version item beside it /// (`latest_versions_read` helper version 1). Without `include_contracts`, the unproved diff --git a/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs b/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs index d1f16b2c60a..bc91deb6069 100644 --- a/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs +++ b/packages/rs-drive-proof-verifier/src/types/data_contracts_latest_versions.rs @@ -19,7 +19,7 @@ pub struct DataContractLatestVersion { /// The current versions of the requested data contracts (`getDataContractsLatestVersions`), /// keyed by contract id. /// -/// Every requested id is a key. A value is `None` when no contract has that id, and +/// Every distinct requested id is a key. A value is `None` when no contract has that id, and /// `Some(entry)` otherwise; the entry carries the contract only when the request asked for /// it. #[derive(Debug, Default, Clone, PartialEq)] diff --git a/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs b/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs index 134c430d922..1b1dc697444 100644 --- a/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/prove/prove_contracts_versions/v0/mod.rs @@ -18,6 +18,13 @@ impl Drive { "no contract ids to prove versions for", ))); } + if contract_ids.len() > u16::MAX as usize { + return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!( + "at most {} contract versions can be proved at once, got {}", + u16::MAX, + contract_ids.len() + )))); + } let path_query = Self::fetch_contracts_versions_query(contract_ids); diff --git a/packages/rs-drive/src/drive/contract/queries.rs b/packages/rs-drive/src/drive/contract/queries.rs index 8f1c7cc4460..6e0fa743f8e 100644 --- a/packages/rs-drive/src/drive/contract/queries.rs +++ b/packages/rs-drive/src/drive/contract/queries.rs @@ -92,7 +92,9 @@ impl Drive { /// It selects the requested contract ids under the contracts root and descends the /// subquery key `2`, the four-byte version item every contract carries beside its /// serialized form or history subtree. A requested id no contract has is proved absent. - /// Duplicate ids are folded, so the limit is the number of distinct ids. + /// Duplicate ids are folded, so the limit is the number of distinct ids; the prover and + /// the verifier reject more distinct ids than a query limit can hold, so the saturation + /// here is never reached. /// /// Shared by the prover and the verifier, which must rebuild the exact query. /// @@ -106,7 +108,11 @@ impl Drive { query.set_subquery_key(vec![CONTRACT_VERSION_KEY]); PathQuery::new( vec![Into::<&[u8; 1]>::into(RootTree::DataContractDocuments).to_vec()], - SizedQuery::new(query, Some(distinct_ids.len() as u16), None), + SizedQuery::new( + query, + Some(u16::try_from(distinct_ids.len()).unwrap_or(u16::MAX)), + None, + ), ) } diff --git a/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs b/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs index 80f81eb3bbc..951fb3985a0 100644 --- a/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs +++ b/packages/rs-drive/src/verify/contract/verify_contracts_versions/v0/mod.rs @@ -30,6 +30,13 @@ impl Drive { "no contract ids to verify versions for", ))); } + if contract_ids.len() > u16::MAX as usize { + return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!( + "at most {} contract versions can be verified at once, got {}", + u16::MAX, + contract_ids.len() + )))); + } let requested: BTreeSet<[u8; 32]> = contract_ids.iter().copied().collect(); let path_query = Self::fetch_contracts_versions_query(contract_ids); @@ -276,6 +283,24 @@ mod tests { )); } + #[test] + fn should_reject_more_ids_than_a_query_limit_can_hold() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let ids: Vec<[u8; 32]> = (0..=u16::MAX as u32) + .map(|i| { + let mut id = [0u8; 32]; + id[..4].copy_from_slice(&i.to_be_bytes()); + id + }) + .collect(); + + assert!(drive + .prove_contracts_versions(&ids, None, platform_version) + .is_err()); + assert!(Drive::verify_contracts_versions(&[], &ids, platform_version).is_err()); + } + #[test] fn should_reject_empty_ids() { let drive = setup_drive_with_initial_state_structure(None); From f00ee141f3be38ec83063bd60f3420a08aec2fb6 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 01:22:09 +0700 Subject: [PATCH 05/10] test(drive): re-pin query test app hashes for the PV14 contract version item The DPNS, family, family-with-nulls and withdrawals query integration tests pin the app hash after storing their contracts under the latest protocol version. From protocol version 14 the contract's version item is one more element under the contract's root subtree, so those fourteen pinned hashes move; the first version and protocol version 13 variants keep theirs. Co-Authored-By: Claude Fable 5.1 --- packages/rs-drive/tests/query_tests.rs | 90 +++++++++++++++++--------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index 977b4a70b70..88f6e15893d 100644 --- a/packages/rs-drive/tests/query_tests.rs +++ b/packages/rs-drive/tests/query_tests.rs @@ -2704,9 +2704,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 53, 9, 163, 92, 116, 134, 17, 186, 21, 68, 156, 162, 47, 181, 214, 162, 253, 4, 246, 8, - 41, 187, 151, 152, 216, 164, 206, 110, 230, 176, 124, 225, + 115, 71, 32, 19, 19, 28, 185, 173, 19, 83, 31, 196, 218, 150, 103, 242, 170, 82, 31, + 219, 106, 186, 227, 137, 222, 8, 119, 190, 132, 141, 5, 129, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -4025,11 +4027,13 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash assert_eq!( root_hash.as_slice(), vec![ - 144, 154, 147, 246, 236, 57, 41, 67, 21, 26, 212, 158, 68, 159, 206, 26, 158, 50, - 252, 62, 143, 176, 149, 50, 19, 226, 239, 65, 112, 243, 225, 64 + 219, 212, 38, 199, 221, 53, 199, 246, 2, 67, 249, 145, 196, 196, 101, 196, 78, 109, + 123, 83, 87, 87, 128, 249, 130, 7, 56, 229, 223, 246, 8, 76, ], ); } @@ -4179,9 +4183,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 53, 9, 163, 92, 116, 134, 17, 186, 21, 68, 156, 162, 47, 181, 214, 162, 253, 4, 246, 8, - 41, 187, 151, 152, 216, 164, 206, 110, 230, 176, 124, 225, + 115, 71, 32, 19, 19, 28, 185, 173, 19, 83, 31, 196, 218, 150, 103, 242, 170, 82, 31, + 219, 106, 186, 227, 137, 222, 8, 119, 190, 132, 141, 5, 129, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -5053,9 +5059,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 75, 38, 164, 96, 117, 46, 13, 23, 183, 41, 83, 163, 112, 55, 172, 37, 186, 36, 223, 39, - 106, 201, 46, 222, 167, 79, 236, 122, 12, 210, 29, 123, + 119, 63, 228, 222, 146, 3, 214, 206, 200, 85, 238, 169, 61, 76, 179, 15, 177, 234, 189, + 243, 215, 36, 123, 160, 213, 152, 77, 100, 96, 29, 85, 193, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -5177,9 +5185,11 @@ mod tests { .expect("there is always a root hash"); // Make sure the state is deterministic + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 53, 9, 163, 92, 116, 134, 17, 186, 21, 68, 156, 162, 47, 181, 214, 162, 253, 4, 246, 8, - 41, 187, 151, 152, 216, 164, 206, 110, 230, 176, 124, 225, + 115, 71, 32, 19, 19, 28, 185, 173, 19, 83, 31, 196, 218, 150, 103, 242, 170, 82, 31, + 219, 106, 186, 227, 137, 222, 8, 119, 190, 132, 141, 5, 129, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -6151,9 +6161,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 235, 23, 161, 209, 153, 68, 160, 57, 151, 170, 19, 99, 64, 48, 5, 114, 233, 154, 77, - 65, 104, 102, 128, 181, 159, 124, 54, 108, 229, 88, 185, 134, + 72, 10, 0, 120, 37, 84, 157, 63, 20, 182, 20, 142, 211, 145, 233, 100, 29, 203, 146, + 87, 162, 128, 198, 53, 155, 57, 42, 81, 94, 17, 83, 240, ]; assert_eq!(root_hash.as_slice(), expected_app_hash,); @@ -6248,9 +6260,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 235, 23, 161, 209, 153, 68, 160, 57, 151, 170, 19, 99, 64, 48, 5, 114, 233, 154, 77, - 65, 104, 102, 128, 181, 159, 124, 54, 108, 229, 88, 185, 134, + 72, 10, 0, 120, 37, 84, 157, 63, 20, 182, 20, 142, 211, 145, 233, 100, 29, 203, 146, + 87, 162, 128, 198, 53, 155, 57, 42, 81, 94, 17, 83, 240, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -6345,9 +6359,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 235, 23, 161, 209, 153, 68, 160, 57, 151, 170, 19, 99, 64, 48, 5, 114, 233, 154, 77, - 65, 104, 102, 128, 181, 159, 124, 54, 108, 229, 88, 185, 134, + 72, 10, 0, 120, 37, 84, 157, 63, 20, 182, 20, 142, 211, 145, 233, 100, 29, 203, 146, + 87, 162, 128, 198, 53, 155, 57, 42, 81, 94, 17, 83, 240, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -6442,9 +6458,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 235, 23, 161, 209, 153, 68, 160, 57, 151, 170, 19, 99, 64, 48, 5, 114, 233, 154, 77, - 65, 104, 102, 128, 181, 159, 124, 54, 108, 229, 88, 185, 134, + 72, 10, 0, 120, 37, 84, 157, 63, 20, 182, 20, 142, 211, 145, 233, 100, 29, 203, 146, + 87, 162, 128, 198, 53, 155, 57, 42, 81, 94, 17, 83, 240, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -6638,9 +6656,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 233, 90, 110, 8, 43, 137, 139, 242, 8, 152, 175, 246, 177, 73, 49, 137, 61, 142, 2, 49, - 158, 134, 13, 222, 60, 223, 139, 41, 66, 131, 135, 38, + 191, 77, 82, 194, 247, 92, 67, 134, 235, 75, 244, 116, 69, 154, 189, 12, 249, 5, 25, + 139, 146, 147, 153, 24, 198, 87, 19, 147, 13, 11, 118, 10, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -6846,9 +6866,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 233, 90, 110, 8, 43, 137, 139, 242, 8, 152, 175, 246, 177, 73, 49, 137, 61, 142, 2, 49, - 158, 134, 13, 222, 60, 223, 139, 41, 66, 131, 135, 38, + 191, 77, 82, 194, 247, 92, 67, 134, 235, 75, 244, 116, 69, 154, 189, 12, 249, 5, 25, + 139, 146, 147, 153, 24, 198, 87, 19, 147, 13, 11, 118, 10, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -7064,9 +7086,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 233, 90, 110, 8, 43, 137, 139, 242, 8, 152, 175, 246, 177, 73, 49, 137, 61, 142, 2, 49, - 158, 134, 13, 222, 60, 223, 139, 41, 66, 131, 135, 38, + 191, 77, 82, 194, 247, 92, 67, 134, 235, 75, 244, 116, 69, 154, 189, 12, 249, 5, 25, + 139, 146, 147, 153, 24, 198, 87, 19, 147, 13, 11, 118, 10, ]; assert_eq!(root_hash.as_slice(), expected_app_hash,); @@ -7281,9 +7305,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 224, 113, 139, 209, 16, 163, 116, 151, 213, 152, 169, 13, 158, 228, 31, 124, 88, 139, - 165, 2, 152, 27, 85, 54, 21, 40, 183, 80, 104, 140, 198, 119, + 205, 146, 93, 135, 221, 204, 196, 241, 245, 249, 201, 39, 63, 75, 164, 201, 54, 207, + 217, 96, 254, 99, 76, 95, 100, 185, 69, 107, 39, 249, 103, 12, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -7362,9 +7388,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 224, 113, 139, 209, 16, 163, 116, 151, 213, 152, 169, 13, 158, 228, 31, 124, 88, 139, - 165, 2, 152, 27, 85, 54, 21, 40, 183, 80, 104, 140, 198, 119, + 205, 146, 93, 135, 221, 204, 196, 241, 245, 249, 201, 39, 63, 75, 164, 201, 54, 207, + 217, 96, 254, 99, 76, 95, 100, 185, 69, 107, 39, 249, 103, 12, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); @@ -7467,9 +7495,11 @@ mod tests { .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash let expected_app_hash = vec![ - 224, 113, 139, 209, 16, 163, 116, 151, 213, 152, 169, 13, 158, 228, 31, 124, 88, 139, - 165, 2, 152, 27, 85, 54, 21, 40, 183, 80, 104, 140, 198, 119, + 205, 146, 93, 135, 221, 204, 196, 241, 245, 249, 201, 39, 63, 75, 164, 201, 54, 207, + 217, 96, 254, 99, 76, 95, 100, 185, 69, 107, 39, 249, 103, 12, ]; assert_eq!(root_hash.as_slice(), expected_app_hash); From a00d5c55637c3567b4bd994c86d3a5ec7639470e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 01:25:57 +0700 Subject: [PATCH 06/10] test(drive-abci): expect the PV14 vote resolution fund in the specialized funds strategy test Pre-existing on v4.2-dev (its own workspace test job fails on this test at 28e70452eb): protocol version 14 lowers the contested document contribution to the vote resolution fund from 0.2 DASH to 0.1 DASH (VOTE_RESOLUTION_FUND_FEES v2), so two contenders fund 20_000_000_000 credits and the leftover distributed to the processing pool when the vote finishes is 19_810_000_000, not 39_810_000_000. Fixed here so the workspace test check can pass. Co-Authored-By: Claude Fable 5.1 --- .../tests/strategy_tests/test_cases/voting_tests.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/voting_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/voting_tests.rs index 00808d59dce..a9e8a37d696 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/voting_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/voting_tests.rs @@ -2146,9 +2146,12 @@ mod tests { // A vote costs 10_000_000 // We did 5 votes in this epoch, - // We had 39_810_000_000 left over, which is only the cost of 19 votes - // So we basically have 39_810_000_000 + 50_000_000 - assert_eq!(processing_fees, 39_860_000_000); + // From protocol version 14 each contested document contributes 0.1 DASH + // (10_000_000_000) to the vote resolution fund instead of 0.2 DASH, so the two + // contenders funded 20_000_000_000, of which 19 votes cost 190_000_000 and + // 19_810_000_000 was left over when the vote finished. + // So we basically have 19_810_000_000 + 50_000_000 + assert_eq!(processing_fees, 19_860_000_000); } #[stack_size(STACK_SIZE)] From 2768a3857345e65ec83a9d529c29d129d50522bb Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 01:41:12 +0700 Subject: [PATCH 07/10] test(drive): re-pin the history query app hashes for the PV14 contract version item The latest-version historical query test pins the app hash twice after storing its contract under the latest protocol version; both move with the version item stored beside the contract from protocol version 14. The first-version test keeps its hashes. Co-Authored-By: Claude Fable 5.1 --- packages/rs-drive/tests/query_tests_history.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive/tests/query_tests_history.rs b/packages/rs-drive/tests/query_tests_history.rs index 604658a97f0..c527f9ce127 100644 --- a/packages/rs-drive/tests/query_tests_history.rs +++ b/packages/rs-drive/tests/query_tests_history.rs @@ -1765,11 +1765,13 @@ fn test_query_historical_latest_platform_version() { .root_hash(Some(&db_transaction), &platform_version.drive.grove_version) .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash assert_eq!( root_hash.as_slice(), vec![ - 161, 240, 182, 38, 13, 26, 246, 165, 76, 67, 252, 39, 203, 128, 225, 233, 70, 76, 30, - 228, 64, 40, 59, 240, 240, 135, 215, 135, 146, 2, 128, 65 + 54, 181, 217, 82, 201, 205, 230, 125, 186, 252, 4, 136, 247, 204, 234, 159, 14, 239, + 59, 72, 69, 192, 182, 54, 122, 100, 192, 158, 183, 88, 31, 255, ] ); @@ -3141,11 +3143,13 @@ fn test_query_historical_latest_platform_version() { .root_hash(Some(&db_transaction), &platform_version.drive.grove_version) .unwrap() .expect("there is always a root hash"); + // protocol version 14 stores the contract's version item beside the contract, one more + // element under the contract's root subtree in the app hash assert_eq!( root_hash.as_slice(), vec![ - 82, 200, 76, 76, 113, 4, 94, 39, 105, 206, 63, 185, 209, 222, 13, 161, 194, 209, 156, - 251, 133, 192, 38, 65, 93, 196, 214, 198, 52, 196, 37, 208 + 74, 167, 180, 31, 0, 73, 101, 156, 93, 253, 230, 154, 157, 52, 205, 74, 148, 69, 143, + 223, 85, 165, 216, 188, 121, 29, 94, 15, 126, 126, 39, 199, ] ); } From cd62abed6f672444a11a0543f646ad7a9abbe14d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 02:17:37 +0700 Subject: [PATCH 08/10] test(wasm-sdk): assert addKnownContract's result with the suite's callable assertions Pre-existing on v4.2-dev since #4746 (the functional tests job fails on it at 9361d4c764): the functional tests use callable assertions (`to.be.true()`), where `to.be(true)` is not a function. Co-Authored-By: Claude Fable 5.1 --- .../tests/functional/contracts.spec.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/wasm-sdk/tests/functional/contracts.spec.ts b/packages/wasm-sdk/tests/functional/contracts.spec.ts index 9f77ef97daa..f311d92a819 100644 --- a/packages/wasm-sdk/tests/functional/contracts.spec.ts +++ b/packages/wasm-sdk/tests/functional/contracts.spec.ts @@ -135,7 +135,27 @@ describe('Data Contract Queries', function describeDataContractQueries() { describe('addKnownContract()', () => { it('should accept a contract the caller holds', async () => { const contract = await client.getDataContract(dpnsContractId); - expect(client.addKnownContract(contract)).to.be(true); + expect(client.addKnownContract(contract)).to.be.true(); + }); + }); + + describe('getDataContractsLatestVersions() on a fresh client', () => { + // The staleness check is the first thing an app asks after start-up, and a client + // seeded below protocol version 14 has not ratcheted yet: the verifier must take the + // proof shape from the responding node's protocol version, not from the client's. + it('should verify the version proof when pinned to protocol version 13', async () => { + const context = await prefetchLocalReady(); + const freshClient = await sdk.WasmSdkBuilder.local() + .withTrustedContext(context) + .withVersion(13) + .build(); + try { + const res = await freshClient.getDataContractsLatestVersions({ contractIds: [dpnsContractId] }); + expect(res.get(dpnsContractId).version).to.be.at.least(1); + expect(res.get(dpnsContractId).dataContract).to.be.undefined(); + } finally { + freshClient.free(); + } }); }); From 299facc3ec8f9035eaa9220e744f30062d20d396 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 02:17:37 +0700 Subject: [PATCH 09/10] fix(drive-proof-verifier): take the contract versions proof shape from the responding node's protocol version The SDK seeds mainnet, testnet and regtest clients at protocol version 13 and ratchets only after a proof verifies. A fresh client whose first proved call is getDataContractsLatestVersions would verify with the version 13 helper, expect the multi-contract proof, and reject the version item proof a version 14 network returns, so it could never ratchet through this query. The verifier now selects the shape from the protocol version the response metadata carries, which the quorum signature covers; a wrong shape can only fail verification. A client already at 14 likewise accepts the multi-contract proof of a network that has not activated yet. A wasm-sdk functional test pins a fresh local client to version 13 and makes this query its first call. Co-Authored-By: Claude Fable 5.1 --- .../proof/data_contracts_latest_versions.rs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs b/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs index 73a0343512f..b1f64a02eef 100644 --- a/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs +++ b/packages/rs-drive-proof-verifier/src/proof/data_contracts_latest_versions.rs @@ -54,7 +54,16 @@ impl FromProof for DataContractsLatestVer // that, and whenever the contracts were asked for, the proof is the multi-contract // proof, which carries the contracts: each version is read off the verified contract, // and the contract is kept only when it was asked for. - let from_version_items = match platform_version + // + // The shape follows the protocol version the responding node ran at, which the + // metadata carries and the quorum signature covers, not the version this client is + // seeded with: a client still at 13 must accept the version item proof of an upgraded + // network (its version only ratchets after a proof verifies), and a client already at + // 14 must accept the multi-contract proof of a network that has not activated yet. A + // wrong shape can only fail verification, never pass a bad proof. + let response_platform_version = + PlatformVersion::get(mtd.protocol_version).unwrap_or(platform_version); + let from_version_items = match response_platform_version .drive_abci .query .data_contract_query_helpers @@ -74,9 +83,12 @@ impl FromProof for DataContractsLatestVer let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; let versions = if from_version_items { - let (root_hash, versions) = - Drive::verify_contracts_versions(proof_bytes, ids.as_slice(), platform_version) - .map_drive_error(proof, mtd)?; + let (root_hash, versions) = Drive::verify_contracts_versions( + proof_bytes, + ids.as_slice(), + response_platform_version, + ) + .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -92,9 +104,13 @@ impl FromProof for DataContractsLatestVer }) .collect::>()? } else { - let (root_hash, contracts) = - Drive::verify_contracts(proof_bytes, false, ids.as_slice(), platform_version) - .map_drive_error(proof, mtd)?; + let (root_hash, contracts) = Drive::verify_contracts( + proof_bytes, + false, + ids.as_slice(), + response_platform_version, + ) + .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; From 496f4092f3a4c5d57a20b7d31fecfba0702e08d9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 02:27:14 +0700 Subject: [PATCH 10/10] test(platform-test-suite): expect the PV14 withdrawal deletion message Pre-existing on v4.2-dev since #4737 (the end-to-end suite job fails on it at 9361d4c764): the protocol version 14 withdrawals data trigger allows deleting FAILED withdrawals too, and says so in its error message. Co-Authored-By: Claude Fable 5.1 --- packages/platform-test-suite/test/e2e/withdrawals.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform-test-suite/test/e2e/withdrawals.spec.js b/packages/platform-test-suite/test/e2e/withdrawals.spec.js index b7afc0fb2c7..6c8db807f41 100644 --- a/packages/platform-test-suite/test/e2e/withdrawals.spec.js +++ b/packages/platform-test-suite/test/e2e/withdrawals.spec.js @@ -265,7 +265,7 @@ describe('Withdrawals', function withdrawalsTest() { expect.fail('should throw broadcast error'); } catch (e) { - expect(e.message).to.be.equal('withdrawal deletion is allowed only for COMPLETE statuses'); + expect(e.message).to.be.equal('withdrawal deletion is allowed only for COMPLETE or FAILED statuses'); expect(e.code).to.equal(40500); } });