diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_update/mod.rs index 04e7511deed..cb5f4e89844 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_update/mod.rs @@ -1976,6 +1976,343 @@ mod tests { ); } + /// A contract-level bound ENCRYPTION or DECRYPTION key whose contract opts in with + /// `MultipleReferenceToLatest` (2): the mode the JS SDK publishes by default. Drive used + /// to write the current-key alias one level above the purpose subtree, where its sibling + /// reference could not resolve, so the update died as an `InternalError`. + #[tokio::test] + async fn test_identity_update_adding_contract_bound_key_multiple_reference_to_latest() { + use crate::execution::validation::state_transition::tests::{ + register_contract_from_bytes, IdentityTestInfo, + }; + use drive::config::DriveConfig; + use drive::drive::identity::key::fetch::{ + IdentityKeysRequest, KeyIDVec, KeyKindRequestType, KeyRequestType, + }; + + let platform_config = PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + // Consistency verification makes GroveDB reject two pending operations on one + // slot, so a duplicated alias write would fail here instead of silently winning. + drive: DriveConfig { + batching_consistency_verification: true, + ..Default::default() + }, + ..Default::default() + }; + + let platform_version = PlatformVersion::latest(); + + let mut platform = TestPlatformBuilder::new() + .with_config(platform_config) + .build_with_mock_rpc() + .set_genesis_state(); + + // The bytes below decode to this contract (bound-key requirements `0`); the test + // flips both requirements to `2` before registering it. + // { + // "$formatVersion": "1", + // "id": "5pkMhyeaFjJfVMkFhLtJdDp2ofx6iqt7i9k6ckkHBwbs", + // "config": { + // "$formatVersion": "1", + // "canBeDeleted": false, + // "readonly": false, + // "keepsHistory": false, + // "documentsKeepHistoryContractDefault": false, + // "documentsMutableContractDefault": true, + // "documentsCanBeDeletedContractDefault": true, + // "requiresIdentityEncryptionBoundedKey": 0, + // "requiresIdentityDecryptionBoundedKey": 0, + // "sizedIntegerTypes": true + // }, + // "version": 1, + // "ownerId": "DicUmimv71VqxNBzZHXb887RgssSEjjx7DyLfxrt8q1X", + // "schemaDefs": null, + // "documentSchemas": { + // "preorder": { + // "documentsMutable": false, + // "canBeDeleted": true, + // "type": "object", + // "indices": [ + // { + // "name": "saltedHash", + // "properties": [ + // { + // "saltedDomainHash": "asc" + // } + // ], + // "unique": true + // } + // ], + // "properties": { + // "saltedDomainHash": { + // "type": "array", + // "byteArray": true, + // "minItems": 32, + // "maxItems": 32, + // "position": 0, + // "description": "Double sha-256 of the concatenation of a 32 byte random salt and a normalized domain name" + // } + // }, + // "required": [ + // "saltedDomainHash" + // ], + // "additionalProperties": false, + // "$comment": "Preorder documents are immutable: modification and deletion are restricted" + // } + // }, + // "createdAt": 1749816974718, + // "updatedAt": null, + // "createdAtBlockHeight": 159130, + // "updatedAtBlockHeight": null, + // "createdAtEpoch": 7906, + // "updatedAtEpoch": null, + // "groups": {}, + // "tokens": {}, + // "keywords": [], + // "description": null + // } + let contract_bytes = hex::decode("0147aa11d517710d509edaf84bb54902394dcb8f6cc68775138d1cdd8334600d2e01000000000101010001000101bcf52c1c5d57d2e21530c5d03ef4c6e7b39a91da7c444fdb17e0a7746b6285860001087072656f7264657216081210646f63756d656e74734d757461626c651300120c63616e426544656c65746564130012047479706512066f626a6563741207696e64696365731501160312046e616d65120a73616c74656448617368120a70726f7065727469657315011601121073616c746564446f6d61696e4861736812036173631206756e697175651301120a70726f706572746965731601121073616c746564446f6d61696e486173681606120474797065120561727261791209627974654172726179130112086d696e4974656d73022012086d61784974656d7302201208706f736974696f6e0200120b6465736372697074696f6e1259446f75626c65207368612d323536206f662074686520636f6e636174656e6174696f6e206f66206120333220627974652072616e646f6d2073616c7420616e642061206e6f726d616c697a656420646f6d61696e206e616d65120872657175697265641501121073616c746564446f6d61696e4861736812146164646974696f6e616c50726f706572746965731300120824636f6d6d656e74124a5072656f7264657220646f63756d656e74732061726520696d6d757461626c653a206d6f64696669636174696f6e20616e642064656c6574696f6e20617265207265737472696374656401fd0000019769381d7e0001fc00026d9a0001fb1ee20000000000").expect("expected to decode contract bytes"); + + let (identity, signer, critical_key, master_key) = + setup_identity_return_master_key(&mut platform, 958, dash_to_credits!(5.0)); + + let platform_state = platform.state.load(); + + // Same contract, but opting bound ENCRYPTION and DECRYPTION keys in with + // `MultipleReferenceToLatest` (2), the mode the DashPay contract and every + // contract published from the JS SDK use, instead of `Unique` (0). + let contract_bytes = { + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; + use dpp::serialization::{ + PlatformDeserializableWithPotentialValidationFromVersionedStructureUntrusted, + PlatformSerializableWithPlatformVersion, + }; + let mut contract = dpp::data_contract::DataContract::versioned_deserialize_untrusted( + &contract_bytes, + false, + platform_version, + ) + .expect("expected to deserialize data contract"); + contract + .config_mut() + .set_requires_identity_encryption_bounded_key(Some( + StorageKeyRequirements::MultipleReferenceToLatest, + )); + contract + .config_mut() + .set_requires_identity_decryption_bounded_key(Some( + StorageKeyRequirements::MultipleReferenceToLatest, + )); + contract + .serialize_to_bytes_with_platform_version(platform_version) + .expect("expected to serialize data contract") + }; + + // Register the contract + let data_contract = register_contract_from_bytes( + &mut platform, + &platform_state, + contract_bytes, + IdentityTestInfo::Given { + identity: &identity, + signer: &signer, + public_key: &critical_key, + identity_nonce: 1, + }, + platform_version, + ) + .await; + + let bounds = ContractBounds::SingleContract { + id: data_contract.id(), + }; + let secp = Secp256k1::new(); + let mut rng = StdRng::seed_from_u64(1292); + let pairs: Vec<(u32, Purpose, Keypair)> = vec![ + (2, Purpose::ENCRYPTION, Keypair::new(&secp, &mut rng)), + (3, Purpose::DECRYPTION, Keypair::new(&secp, &mut rng)), + ]; + let mut adds: Vec = pairs + .iter() + .map(|(id, purpose, pair)| IdentityPublicKeyInCreationV0 { + id: *id, + purpose: *purpose, + security_level: SecurityLevel::MEDIUM, + key_type: ECDSA_SECP256K1, + read_only: false, + data: pair.public_key().serialize().to_vec().into(), + signature: Default::default(), + contract_bounds: Some(bounds.clone()), + }) + .collect(); + let unsigned = |revision: u64, + nonce: u64, + add: Vec, + disable: Vec| + -> StateTransition { + IdentityUpdateTransition::from(IdentityUpdateTransitionV0 { + identity_id: identity.id(), + revision, + nonce, + add_public_keys: add + .into_iter() + .map(IdentityPublicKeyInCreation::V0) + .collect(), + disable_public_keys: disable, + user_fee_increase: 0, + signature_public_key_id: master_key.id(), + signature: Default::default(), + }) + .into() + }; + let apply = |transition: &StateTransition, time_ms: u64| { + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &vec![transition.serialize_to_bytes().unwrap()], + &platform_state, + &BlockInfo { + time_ms, + ..Default::default() + }, + &transaction, + platform_version, + true, + None, + ) + .expect("expected to process state transition"); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }], + "update at {time_ms} must apply" + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + }; + let key_ids = |purpose: Purpose, kind: KeyKindRequestType| -> Vec { + platform + .drive + .fetch_identity_keys::( + IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::ContractBoundKey( + data_contract.id().to_buffer(), + purpose, + kind, + ), + limit: Some(16), + offset: None, + }, + None, + platform_version, + ) + .expect("expected to fetch bound key ids") + }; + let fetch_key = |key_id: u32| { + platform + .drive + .fetch_identity_keys_as_partial_identity( + IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::SpecificKeys(vec![key_id]), + limit: Some(1), + offset: None, + }, + None, + platform_version, + ) + .expect("expected to fetch identity") + .expect("expected identity to exist") + .loaded_public_keys + .remove(&key_id) + .expect("expected the key") + }; + let assert_no_grovedb_issues = || { + let issues = platform + .drive + .grove + .visualize_verify_grovedb(None, true, false, &platform_version.drive.grove_version) + .expect("expected to verify grovedb"); + assert!(issues.is_empty(), "grovedb issues: {issues:?}"); + }; + + // Register both keys in one update: one current-key alias per purpose subtree. + let signable = unsigned(1, 2, adds.clone(), vec![]) + .signable_bytes() + .expect("expected signable bytes"); + for (key, (_, _, pair)) in adds.iter_mut().zip(&pairs) { + key.signature = signer::sign(&signable, &pair.secret_key().secret_bytes()) + .expect("expected to sign") + .to_vec() + .into(); + } + let mut registration = unsigned(1, 2, adds, vec![]); + registration.set_signature( + signer + .sign(&master_key, &signable) + .await + .expect("expected to sign"), + ); + apply(®istration, 1000); + assert_no_grovedb_issues(); + + for (id, purpose, _) in &pairs { + assert_eq!(fetch_key(*id).contract_bounds(), Some(&bounds)); + assert_eq!( + key_ids(*purpose, KeyKindRequestType::CurrentKeyOfKindRequest), + vec![*id], + "{purpose:?} current key" + ); + assert_eq!( + key_ids(*purpose, KeyKindRequestType::AllKeysOfKindRequest), + vec![*id], + "{purpose:?} listing must not repeat the current key alias" + ); + } + + // Disabling the encryption key refreshes its references, including the alias. + let mut revocation = unsigned(2, 3, vec![], vec![2]); + revocation.set_signature( + signer + .sign( + &master_key, + &revocation + .signable_bytes() + .expect("expected signable bytes"), + ) + .await + .expect("expected to sign"), + ); + apply(&revocation, 2000); + assert_no_grovedb_issues(); + + assert_eq!(fetch_key(2).disabled_at(), Some(2000)); + assert_eq!( + key_ids( + Purpose::ENCRYPTION, + KeyKindRequestType::CurrentKeyOfKindRequest + ), + vec![2] + ); + assert_eq!( + key_ids( + Purpose::DECRYPTION, + KeyKindRequestType::CurrentKeyOfKindRequest + ), + vec![3] + ); + } + #[tokio::test] async fn test_identity_update_adding_contract_bound_key_on_document_level() { use crate::execution::validation::state_transition::tests::{ diff --git a/packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs b/packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs index e242532a942..0ae92582413 100644 --- a/packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs +++ b/packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs @@ -299,18 +299,17 @@ impl Drive { if storage_key_requirements == StorageKeyRequirements::MultipleReferenceToLatest { // we also insert a sibling reference so we can query the current key + // The alias is a sibling reference, so it must live in the purpose + // subtree beside the key id it names, which is also where the + // current-key query reads it (`CurrentKeyOfKindRequest`). v0 wrote it + // one level up for encryption and decryption keys, where the sibling + // could not resolve, so such keys could never be registered. let sibling_ref_type_path = SiblingReference(key_id_bytes); - let sibling_path = if purpose == Purpose::AUTHENTICATION { - // A bound authentication key's current-key reference belongs beside - // its key IDs, under the purpose subtree. Keep legacy paths frozen. - identity_contract_info_group_path_key_purpose_vec( - &identity_id, - &root_id, - purpose, - ) - } else { - identity_contract_info_group_keys_path_vec(&identity_id, &root_id) - }; + let sibling_path = identity_contract_info_group_path_key_purpose_vec( + &identity_id, + &root_id, + purpose, + ); self.batch_insert( PathKeyElementInfo::<0>::PathKeyElement(( diff --git a/packages/rs-drive/src/drive/identity/contract_info/keys/mod.rs b/packages/rs-drive/src/drive/identity/contract_info/keys/mod.rs index 22ca9b60890..a2fed9b86b7 100644 --- a/packages/rs-drive/src/drive/identity/contract_info/keys/mod.rs +++ b/packages/rs-drive/src/drive/identity/contract_info/keys/mod.rs @@ -77,8 +77,10 @@ pub(crate) fn coalesce_current_key_alias_operations(operations: &mut Vec Option<(&KeyInfoPath, Option)> { @@ -362,10 +364,12 @@ mod tests { &document_type_group, Purpose::AUTHENTICATION, ); - // Legacy encryption and decryption bounds alias the current key at the keys level. - let legacy_alias_path = + // The keys level holds the purpose subtrees; v1 never writes an alias there (v0 did, + // for encryption and decryption keys, where it could not resolve). A sibling reference + // at its empty key is not a slot and must not be recognized. + let keys_level_path = identity_contract_info_group_keys_path_vec(&identity_id, &contract_id); - // A purpose-level alias for another purpose must not be recognized either. + // The alias slot of another purpose is not coalesced either. let encryption_purpose_path = identity_contract_info_group_path_key_purpose_vec( &identity_id, &contract_id, @@ -375,10 +379,10 @@ mod tests { let mut operations = vec![ alias_refresh(contract_auth_path.clone(), 1), alias_insert(contract_auth_path.clone(), 2), - alias_insert(legacy_alias_path.clone(), 5), + alias_insert(keys_level_path.clone(), 5), alias_insert(document_type_auth_path.clone(), 9), alias_insert(contract_auth_path.clone(), 3), - alias_insert(legacy_alias_path.clone(), 6), + alias_insert(keys_level_path.clone(), 6), alias_refresh(encryption_purpose_path.clone(), 7), alias_insert(encryption_purpose_path.clone(), 8), alias_insert(document_type_auth_path.clone(), 10), @@ -390,15 +394,15 @@ mod tests { assert_eq!( operations, vec![ - alias_insert(legacy_alias_path.clone(), 5), + alias_insert(keys_level_path.clone(), 5), alias_insert(contract_auth_path, 3), - alias_insert(legacy_alias_path, 6), + alias_insert(keys_level_path, 6), alias_refresh(encryption_purpose_path.clone(), 7), alias_insert(encryption_purpose_path, 8), alias_insert(document_type_auth_path, 10), ], "each AUTHENTICATION slot keeps the insertion naming its highest key id, in place; \ - legacy and other-purpose aliases are untouched" + keys-level and other-purpose writes are untouched" ); } diff --git a/packages/rs-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs b/packages/rs-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs index 0fe87b6fb96..c4524abafad 100644 --- a/packages/rs-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs +++ b/packages/rs-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs @@ -1,7 +1,6 @@ use crate::drive::identity::contract_info::keys::IdentityDataContractKeyApplyInfo; use crate::drive::identity::{ - identity_contract_info_group_keys_path_vec, identity_contract_info_group_path_key_purpose_vec, - identity_key_location_within_identity_vec, + identity_contract_info_group_path_key_purpose_vec, identity_key_location_within_identity_vec, }; use crate::drive::Drive; use crate::error::contract::DataContractError; @@ -207,19 +206,14 @@ impl Drive { if storage_key_requirements == StorageKeyRequirements::MultipleReferenceToLatest { // we also refresh the sibling reference, so we can query the current key + // The alias lives in the purpose subtree for every purpose, beside the + // key id it names (see `add_contract_info_operations_v1`). let sibling_ref_type_path = SiblingReference(key_id_bytes); - let sibling_path = if purpose == Purpose::AUTHENTICATION { - // A bound authentication key's current-key reference belongs beside - // its key IDs, under the purpose subtree. Legacy purposes keep - // their historical path (see v0). - identity_contract_info_group_path_key_purpose_vec( - &identity_id, - &root_id, - purpose, - ) - } else { - identity_contract_info_group_keys_path_vec(&identity_id, &root_id) - }; + let sibling_path = identity_contract_info_group_path_key_purpose_vec( + &identity_id, + &root_id, + purpose, + ); // Untrusted refresh: the slot may point at a newer key covering the same // contract, so only the stored value hash is rebuilt; a trusted refresh diff --git a/packages/rs-drive/src/drive/identity/key/fetch/mod.rs b/packages/rs-drive/src/drive/identity/key/fetch/mod.rs index 191d552272d..c0c0dce09d8 100644 --- a/packages/rs-drive/src/drive/identity/key/fetch/mod.rs +++ b/packages/rs-drive/src/drive/identity/key/fetch/mod.rs @@ -17,10 +17,7 @@ use { dpp::identity::{KeyID, Purpose, SecurityLevel}, grovedb::{PathQuery, SizedQuery}, integer_encoding::VarInt, - std::{ - collections::{BTreeMap, BTreeSet}, - ops::RangeFull, - }, + std::collections::{BTreeMap, BTreeSet}, }; #[cfg(feature = "server")] @@ -58,11 +55,18 @@ mod fetch_identity_keys; /// A kind is a purpose/security level pair /// Do you want to get all keys in that pair /// Or just the current one? +/// +/// For contract-bound keys the purpose subtree keeps its current key at the empty key: the +/// only key under `StorageKeyRequirements::Unique`, or a sibling reference to the newest key id +/// under `MultipleReferenceToLatest` (every bound authentication key). The key id entries hold +/// every key registered under `Multiple` or `MultipleReferenceToLatest`. #[derive(Clone, Copy)] pub enum KeyKindRequestType { /// Get only the last key of a certain kind CurrentKeyOfKindRequest, - /// Get all keys of a certain kind + /// Get all keys of a certain kind. For contract-bound keys this lists the key id entries, + /// skipping the current-key slot so the newest key is not repeated; a `Unique` bound key + /// has no key id entry and is read with `CurrentKeyOfKindRequest`. AllKeysOfKindRequest, } @@ -952,13 +956,9 @@ impl IdentityKeysRequest { Query::new_single_key(vec![]) } AllKeysOfKindRequest => { - if purpose == Purpose::AUTHENTICATION { - // Bound authentication keys keep their current-key alias at the - // empty key of the purpose subtree; listing must not repeat it. - Query::new_single_query_item(QueryItem::RangeAfter(vec![]..)) - } else { - Query::new_single_query_item(QueryItem::RangeFull(RangeFull)) - } + // Every purpose keeps its current-key alias at the empty key of the + // purpose subtree; listing must not repeat it. + Query::new_single_query_item(QueryItem::RangeAfter(vec![]..)) } }; PathQuery { @@ -989,13 +989,9 @@ impl IdentityKeysRequest { Query::new_single_key(vec![]) } AllKeysOfKindRequest => { - if purpose == Purpose::AUTHENTICATION { - // Bound authentication keys keep their current-key alias at the - // empty key of the purpose subtree; listing must not repeat it. - Query::new_single_query_item(QueryItem::RangeAfter(vec![]..)) - } else { - Query::new_single_query_item(QueryItem::RangeFull(RangeFull)) - } + // Every purpose keeps its current-key alias at the empty key of the + // purpose subtree; listing must not repeat it. + Query::new_single_query_item(QueryItem::RangeAfter(vec![]..)) } }; PathQuery { @@ -1575,15 +1571,201 @@ mod tests { let identity_id: [u8; 32] = [18u8; 32]; let contract_id: [u8; 32] = [19u8; 32]; - let request = IdentityKeysRequest { - identity_id, - request_type: ContractBoundKey(contract_id, Purpose::ENCRYPTION, AllKeysOfKindRequest), - limit: None, - offset: None, + for purpose in [ + Purpose::AUTHENTICATION, + Purpose::ENCRYPTION, + Purpose::DECRYPTION, + ] { + let request = IdentityKeysRequest { + identity_id, + request_type: ContractBoundKey(contract_id, purpose, AllKeysOfKindRequest), + limit: None, + offset: None, + }; + + let path_query = request.into_path_query(); + assert!(path_query.query.limit.is_none()); + // The current-key alias at the empty key must not be listed for any purpose. + assert_eq!( + path_query.query.query.items, + vec![QueryItem::RangeAfter(vec![]..)], + "{purpose:?}" + ); + } + } + + /// A contract-level bound ENCRYPTION or DECRYPTION key stored under + /// `MultipleReferenceToLatest` keeps its current-key alias at the empty key of its purpose + /// subtree, where the current-key query reads it, and the alias must not be repeated when + /// the keys of that kind are listed. + #[test] + fn test_fetch_contract_bound_encryption_and_decryption_keys_with_multiple_reference_to_latest() + { + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; + use dpp::identity::contract_bounds::ContractBounds; + use dpp::identity::{KeyType, Purpose, SecurityLevel}; + use dpp::serialization::PlatformSerializable; + use dpp::tests::fixtures::get_dashpay_contract_fixture; + use rand::SeedableRng; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + let transaction = drive.grove.start_transaction(); + + let identity = Identity::random_identity(2, Some(4242), platform_version) + .expect("expected a random identity"); + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + Some(&transaction), + platform_version, + ) + .expect("expected to insert identity"); + + // The DashPay contract opts document-type keys in with `2`; opt the contract itself in. + let mut contract = + get_dashpay_contract_fixture(Some(identity.id()), 1, platform_version.protocol_version) + .data_contract_owned(); + contract + .config_mut() + .set_requires_identity_encryption_bounded_key(Some( + StorageKeyRequirements::MultipleReferenceToLatest, + )); + contract + .config_mut() + .set_requires_identity_decryption_bounded_key(Some( + StorageKeyRequirements::MultipleReferenceToLatest, + )); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + None, + Some(&transaction), + platform_version, + ) + .expect("expected to apply contract"); + + let bounds = ContractBounds::SingleContract { id: contract.id() }; + let mut rng = rand::rngs::StdRng::seed_from_u64(4243); + let keys: Vec = [ + (2u32, Purpose::ENCRYPTION), + (3, Purpose::DECRYPTION), + (4, Purpose::ENCRYPTION), + ] + .into_iter() + .map(|(id, purpose)| { + IdentityPublicKey::random_key_with_known_attributes( + id, + &mut rng, + purpose, + SecurityLevel::MEDIUM, + KeyType::ECDSA_SECP256K1, + Some(bounds.clone()), + platform_version, + ) + .expect("expected a bound key") + .0 + }) + .collect(); + for key in &keys { + let register = |apply: bool| { + drive + .add_new_unique_keys_to_identity( + identity.id().to_buffer(), + vec![key.clone()], + &BlockInfo::default(), + apply, + Some(&transaction), + platform_version, + ) + .expect("expected to add a bound key") + }; + let estimated = register(false); + let actual = register(true); + // The estimate prices the purpose subtree with room for the alias reference. + assert!( + estimated.processing_fee >= actual.processing_fee, + "estimate {} must cover execution {}", + estimated.processing_fee, + actual.processing_fee + ); + assert!(estimated.storage_fee >= actual.storage_fee); + } + + let key_ids = |purpose: Purpose, kind: KeyKindRequestType| -> KeyIDVec { + drive + .fetch_identity_keys( + IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: ContractBoundKey(contract.id().to_buffer(), purpose, kind), + limit: Some(16), + offset: None, + }, + Some(&transaction), + platform_version, + ) + .expect("expected to fetch bound key ids") + }; + assert_eq!( + key_ids(Purpose::ENCRYPTION, CurrentKeyOfKindRequest), + vec![4], + "the newest encryption key is current" + ); + assert_eq!( + key_ids(Purpose::ENCRYPTION, AllKeysOfKindRequest), + vec![2, 4], + "listing must not repeat the current key alias" + ); + assert_eq!( + key_ids(Purpose::DECRYPTION, CurrentKeyOfKindRequest), + vec![3] + ); + assert_eq!(key_ids(Purpose::DECRYPTION, AllKeysOfKindRequest), vec![3]); + + // `getIdentitiesContractKeys` reads the same current-key slot. + let contract_keys = drive + .fetch_identities_contract_keys( + &[identity.id().to_buffer()], + &contract.id().to_buffer(), + None, + vec![Purpose::ENCRYPTION, Purpose::DECRYPTION], + Some(&transaction), + platform_version, + ) + .expect("expected to fetch the identity's contract keys"); + let expected_current = |key_id: u32| { + keys.iter() + .find(|key| key.id() == key_id) + .expect("expected the key") + .serialize_to_bytes() + .expect("expected to serialize the key") }; + assert_eq!( + contract_keys.get(&identity.id()), + Some(&BTreeMap::from([ + (Purpose::ENCRYPTION, expected_current(4)), + (Purpose::DECRYPTION, expected_current(3)), + ])) + ); - let path_query = request.into_path_query(); - assert!(path_query.query.limit.is_none()); + let issues = drive + .grove + .visualize_verify_grovedb( + Some(&transaction), + true, + false, + &platform_version.drive.grove_version, + ) + .expect("expected to verify grovedb"); + assert!(issues.is_empty(), "grovedb issues: {issues:?}"); } #[test] diff --git a/packages/rs-drive/src/util/operations/apply_batch_low_level_drive_operations/v1/mod.rs b/packages/rs-drive/src/util/operations/apply_batch_low_level_drive_operations/v1/mod.rs index fc2d568163b..d33d62e0753 100644 --- a/packages/rs-drive/src/util/operations/apply_batch_low_level_drive_operations/v1/mod.rs +++ b/packages/rs-drive/src/util/operations/apply_batch_low_level_drive_operations/v1/mod.rs @@ -12,9 +12,10 @@ use std::collections::HashMap; impl Drive { /// Applies a batch of Drive operations to groveDB. /// - /// v1 first coalesces the current-key alias writes of bound authentication keys, so an - /// identity update that registers and revokes keys covering one contract queues a single - /// operation per alias slot (see [`coalesce_current_key_alias_operations`]). + /// v1 first coalesces the current-key alias writes of bound keys stored under + /// `MultipleReferenceToLatest`, so an identity update that registers and revokes keys + /// covering one contract queues a single operation per alias slot (see + /// [`coalesce_current_key_alias_operations`]). pub(crate) fn apply_batch_low_level_drive_operations_v1( &self, estimated_costs_only_with_layer_info: Option< diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs index 56dc684b180..7d431ed83cb 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs @@ -19,7 +19,11 @@ use crate::version::drive_versions::drive_identity_method_versions::{ /// * `contract_info.add_potential_contract_info_for_contract_bounded_key` 0 -> 1 and /// `contract_info.refresh_potential_contract_info_key_references` 0 -> 1: /// write and refresh contract-bound authentication-key references. Both v0s preserve the -/// historical rejection of authentication keys with contract bounds before v14. +/// historical rejection of authentication keys with contract bounds before v14. Both v1s also +/// store the current-key alias of a contract-level encryption or decryption key bound under +/// `MultipleReferenceToLatest` in the key's purpose subtree, where the current-key query reads +/// it; both v0s wrote it one level up, where the sibling reference could not resolve, so such +/// keys could never be registered before v14 (nothing is stored at the v0 path on any network). /// * `keys.insert.insert_new_unique_key` 0 -> 1 and `keys.insert.insert_new_non_unique_key` /// 0 -> 1: a key that carries a budget also gets its remaining budget written to the identity's /// key budgets subtree. Keys cannot carry a budget before v14, so both v0s never write it. diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index c4675fe6688..fc3f7ccfbf9 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -284,7 +284,11 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// Contract-bound authentication keys activate through contract-bounds validation v2, /// identity-signature validation v1 and batch advanced-structure v1. Identity creation /// validates key bounds (state v1) and identity-update state v1 retains the contract -/// lookup fees; Drive identity methods v2 index and refresh the bound keys. +/// lookup fees; Drive identity methods v2 index and refresh the bound keys. The same v1 +/// contract-info methods also store the current-key alias of a contract-level encryption or +/// decryption key bound under `MultipleReferenceToLatest` in its purpose subtree, where the +/// current-key query reads it; v0 wrote it one level up, where its sibling reference could +/// not resolve, so registering such a key failed inside Drive on every earlier version. /// Contract group bounds on authentication keys ride the same versions: contract-bounds /// validation v2 admits them, batch transform v2 resolves the member contract's group /// memberships into the action (only for a group-bound signing key) for advanced-structure v1 to judge, and shielded-proof validation v1 refuses them in identity creation from the