Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions packages/rs-drive/tests/query_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9853,3 +9853,236 @@ mod gapped_index_query_tests {
);
}
}

#[cfg(feature = "server")]
#[cfg(test)]
mod dpns_identity_records_paging {
//! Pins the paging shape the SDK uses to list every DPNS name of an
//! identity (`Sdk::get_all_dpns_usernames_by_identity`): an equality on
//! `records.identity` (the non-unique `identityId` index), no `orderBy`,
//! continued with `startAfter` the previous page's last document id. The
//! SDK treats a short page as "the whole set", so a continuation that
//! drops the rest of the equality bucket would make it prune names.

use super::*;
use dpp::data_contracts::SystemDataContract;
use dpp::system_data_contracts::load_system_data_contract;
use drive::util::object_size_info::DocumentInfo;
use drive::util::test_helpers::setup::setup_drive_with_initial_state_structure;

const IDENTITY: [u8; 32] = [0x11; 32];
const OTHER_IDENTITY: [u8; 32] = [0x22; 32];

/// DPNS contract plus three domains resolving to `IDENTITY` (ids 1, 2, 3)
/// and one resolving elsewhere (id 4), all written at `platform_version`.
fn setup(platform_version: &PlatformVersion) -> (Drive, DataContract) {
let drive = setup_drive_with_initial_state_structure(Some(platform_version));
let contract = load_system_data_contract(SystemDataContract::DPNS, platform_version)
.expect("expected to load the DPNS contract");
drive
.apply_contract(
&contract,
BlockInfo::default(),
true,
None,
None,
platform_version,
)
.expect("expected to apply the DPNS contract");
let document_type = contract
.document_type_for_name("domain")
.expect("expected a domain document type");
for (id, identity) in [
(1u8, IDENTITY),
(2, IDENTITY),
(3, IDENTITY),
(4, OTHER_IDENTITY),
] {
let label = format!("paging-test-name-number-{id}");
let mut document = document_type
.create_document_from_data(
platform_value!({
"label": label.clone(),
"normalizedLabel": label,
"parentDomainName": "dash",
"normalizedParentDomainName": "dash",
"preorderSalt": Bytes32::new([id; 32]),
"records": {"identity": Identifier::from(identity)},
"subdomainRules": {"allowSubdomains": false},
}),
Identifier::from(identity),
1,
1,
[id; 32],
platform_version,
)
.expect("expected to create a domain document");
document.set_id(Identifier::from([id; 32]));
drive
.add_document_for_contract(
DocumentAndContractInfo {
owned_document_info: OwnedDocumentInfo {
document_info: DocumentInfo::DocumentRefInfo((&document, None)),
owner_id: None,
},
contract: &contract,
document_type,
},
true,
BlockInfo::default(),
true,
None,
platform_version,
None,
)
.expect("expected to insert a domain document");
}
(drive, contract)
}

/// One page of `IDENTITY`'s domains through the raw path, the proved
/// path and the client-side proof verification the SDK runs; all three
/// must agree. Returns the document ids of the page.
fn page(
drive: &Drive,
contract: &DataContract,
start_after: Option<u8>,
platform_version: &PlatformVersion,
) -> Vec<Identifier> {
let document_type = contract
.document_type_for_name("domain")
.expect("expected a domain document type");
let mut query_value = json!({
"where": [
["records.identity", "==", bs58::encode(IDENTITY).into_string()]
],
"limit": 2,
});
if let Some(id) = start_after {
query_value["startAfter"] = json!(bs58::encode([id; 32]).into_string());
}
let query_bytes = cbor_serializer::serializable_value_to_cbor(&query_value, None)
.expect("expected to serialize the query");
let query = || {
DriveDocumentQuery::from_cbor(
&query_bytes,
contract,
document_type,
&drive.config,
platform_version,
)
.expect("expected the query to be built")
};
let ids_of = |results: Vec<Vec<u8>>| -> Vec<Identifier> {
results
.iter()
.map(|bytes| {
Document::from_bytes(bytes, document_type, platform_version)
.expect("expected to deserialize a result")
.id()
})
.collect()
};

let (raw, _, _) = query()
.execute_raw_results_no_proof(drive, None, None, platform_version)
.expect("expected the raw query to execute");
let raw = ids_of(raw);

let (_, proved, _) = query()
.execute_with_proof_only_get_elements(drive, None, None, platform_version)
.expect("expected the proved query to execute");
assert_eq!(raw, ids_of(proved), "raw and proved pages must agree");

let (proof, _) = query()
.execute_with_proof(drive, None, None, platform_version)
.expect("expected a proof");
let (_, verified) = query()
.verify_proof(&proof, platform_version)
.expect("expected the proof to verify");
let verified: Vec<Identifier> = verified.iter().map(|document| document.id()).collect();
assert_eq!(raw, verified, "the verified page must match the served one");

raw
}

fn assert_first_page(
drive: &Drive,
contract: &DataContract,
platform_version: &PlatformVersion,
) {
assert_eq!(
page(drive, contract, None, platform_version),
vec![Identifier::from([1; 32]), Identifier::from([2; 32])],
"protocol version {}: first page",
platform_version.protocol_version
);
}

#[test]
fn should_continue_after_a_document_inside_an_equality_bucket_at_latest_protocol_version() {
let platform_version = PlatformVersion::latest();
assert_eq!(
platform_version
.drive
.methods
.document
.query
.non_primary_key_path_query,
1
);
let (drive, contract) = setup(platform_version);

assert_first_page(&drive, &contract, platform_version);
assert_eq!(
page(&drive, &contract, Some(2), platform_version),
vec![Identifier::from([3; 32])],
"startAfter the second document must return the third"
);
assert_eq!(
page(&drive, &contract, Some(1), platform_version),
vec![Identifier::from([2; 32]), Identifier::from([3; 32])],
"startAfter the first document must return the rest of the bucket"
);
assert_eq!(
page(&drive, &contract, Some(3), platform_version),
Vec::<Identifier>::new(),
"startAfter the last document is empty"
);
}

/// The v0 lowering (protocol versions <= 13) hands an exclusive cursor
/// to the equality clause's own key: `WhereClause::to_path_query`'s
/// `Equal` arm drops the key when it equals the cursor document's value
/// and the cursor is not included, which on a single-property index is
/// always the case. Every continuation inside the bucket therefore comes
/// back empty — served, proved and verified alike — so a client cannot
/// tell "no more documents" from "the cursor dropped them", and a full
/// first page is the most a v0 network can be asked for.
///
/// On chain behavior: never edit these expectations; protocol version 14
/// (`non_primary_key_path_query: 1`) is the fix.
#[test]
fn should_drop_the_rest_of_an_equality_bucket_after_start_after_frozen_at_protocol_v13() {
let platform_version = PlatformVersion::get(13).expect("expected protocol version 13");
assert_eq!(
platform_version
.drive
.methods
.document
.query
.non_primary_key_path_query,
0
);
let (drive, contract) = setup(platform_version);

assert_first_page(&drive, &contract, platform_version);
for cursor in [1u8, 2, 3] {
assert_eq!(
page(&drive, &contract, Some(cursor), platform_version),
Vec::<Identifier>::new(),
"v0 drops the whole bucket after startAfter document {cursor}"
);
}
}
}
36 changes: 36 additions & 0 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10921,4 +10921,40 @@ mod tests {
"every emitted marked-used address must carry used == true"
);
}

/// A cold restore must hand every persisted owned DPNS label to the
/// in-memory identity, so a later capped fetch only adds to it and no
/// snapshot emitted before the next complete fetch drops an owned pick
/// that lies outside the fetched prefix.
#[test]
fn restored_identity_carries_its_persisted_dpns_names() {
use super::build_wallet_identity_bucket;
use crate::wallet_restore_types::{IdentityRestoreEntryFFI, WalletRestoreEntryFFI};
use std::ffi::CString;
use std::os::raw::c_char;

let labels: Vec<CString> = ["alice", "bob", "carol"]
.iter()
.map(|l| CString::new(*l).unwrap())
.collect();
let pointers: Vec<*const c_char> = labels.iter().map(|l| l.as_ptr()).collect();
let mut identity: IdentityRestoreEntryFFI = unsafe { std::mem::zeroed() };
identity.identity_id = [7; 32];
identity.dpns_names = pointers.as_ptr();
identity.dpns_names_count = pointers.len();
let entry = WalletRestoreEntryFFI {
wallet_id: [42; 32],
identities: &identity,
identities_count: 1,
..Default::default()
};

let bucket = build_wallet_identity_bucket(&entry).expect("identity must restore");
let restored: Vec<&str> = bucket[&0]
.dpns_names
.iter()
.map(|n| n.label.as_str())
.collect();
assert_eq!(restored, ["alice", "bob", "carol"]);
}
}
2 changes: 1 addition & 1 deletion packages/rs-platform-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub use wallet::identity::{
calculate_account_reference, derive_auto_accept_private_key, derive_contact_payment_address,
derive_contact_payment_addresses, derive_contact_xpub, pubkey_binds_expected_key_data,
unmask_account_reference, BlockTime, ContactProfileEntry, ContactRequest, ContactXpubData,
DashPayProfile, DashPayState, DpnsNameInfo, EstablishedContact, IdentityLocation,
DashPayProfile, DashPayState, DpnsFetch, DpnsNameInfo, EstablishedContact, IdentityLocation,
IdentityManager, IdentityStatus, KeyStorage, ManagedIdentity, PrivateKeyData, ProfileUpdate,
RegistrationIndex, DEFAULT_CONTACT_GAP_LIMIT,
};
Expand Down
6 changes: 3 additions & 3 deletions packages/rs-platform-wallet/src/wallet/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub use state::{
};
pub use types::dashpay::profile::{calculate_avatar_hash, calculate_dhash_fingerprint};
pub use types::{
ContactProfileEntry, ContactRequest, DashPayProfile, DashpayAddressMatch, DpnsNameInfo,
EstablishedContact, IdentityStatus, KeyStorage, PaymentDirection, PaymentEntry, PaymentStatus,
PrivateKeyData, ProfileUpdate,
ContactProfileEntry, ContactRequest, DashPayProfile, DashpayAddressMatch, DpnsFetch,
DpnsNameInfo, EstablishedContact, IdentityStatus, KeyStorage, PaymentDirection, PaymentEntry,
PaymentStatus, PrivateKeyData, ProfileUpdate,
};
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ impl IdentityWallet {
enrichment_deadline: Option<std::time::Instant>,
) -> Result<Vec<Identity>, PlatformWalletError> {
use super::identity_handle::{derive_identity_auth_key_hash_from_master, MASTER_KEY_INDEX};
use crate::wallet::identity::state::managed_identity::key_storage::DpnsNameInfo;
use crate::wallet::identity::state::managed_identity::key_storage::IdentityStatus;
use dash_sdk::platform::types::identity::PublicKeyHash;
use dash_sdk::platform::Fetch;
Expand Down Expand Up @@ -580,12 +579,8 @@ impl IdentityWallet {
break;
}
let identity_id = identity.id();
match self
.sdk
.get_dpns_usernames_by_identity(identity_id, None)
.await
{
Ok(usernames) => {
match self.fetch_owned_dpns_names(identity_id, None).await {
Ok((names, fetch)) => {
let mut wm_guard = self.wallet_manager.write().await;
let info_guard =
wm_guard
Expand All @@ -599,15 +594,7 @@ impl IdentityWallet {
.identity_manager
.managed_identity_mut(&identity_id)
{
for username in usernames {
managed.add_dpns_name(
DpnsNameInfo {
label: username.label,
acquired_at: None,
},
&self.persister,
);
}
managed.apply_fetched_dpns_names(names, fetch, &self.persister);
}
}
Err(e) => {
Expand Down
Loading
Loading