Skip to content
Open
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//! v1 of `validate_identity_public_keys_contract_bounds`.
//!
//! Two behavioral changes vs. v0, both gated together at PROTOCOL_VERSION_12:
//!
//! 1. **Correctness fix.** The `Purpose::DECRYPTION` arm of
//! `ContractBounds::SingleContractDocumentType` now consults
//! `requires_identity_decryption_bounded_key()` instead of v0's mistaken
//! `requires_identity_encryption_bounded_key()`.
//!
//! 2. **Fee accounting (audit N6/N7, follow-up to #3670 / issue #3673).**
//! Every contract fetch and identity-key lookup performed during validation
//! is now billed to the passed-in execution context, instead of being
//! discarded as v0 did (which had an explicit `//todo:` for this).

use crate::error::Error;
use crate::execution::types::execution_operation::ValidationOperation;
use crate::execution::types::state_transition_execution_context::{
StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0,
};
use dpp::block::epoch::Epoch;
use dpp::consensus::basic::document::{DataContractNotPresentError, InvalidDocumentTypeError};
use dpp::consensus::basic::identity::{
DataContractBoundsNotPresentError, InvalidKeyPurposeForContractBoundsError,
};
use dpp::consensus::basic::BasicError;
use dpp::consensus::state::identity::identity_public_key_already_exists_for_unique_contract_bounds_error::IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError;
use dpp::consensus::state::state_error::StateError;
use dpp::consensus::ConsensusError;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::config::v0::DataContractConfigGettersV0;
use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
use dpp::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements;
use dpp::identifier::Identifier;
use dpp::identity::contract_bounds::ContractBounds;
use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
use dpp::identity::Purpose;
use dpp::identity::Purpose::{DECRYPTION, ENCRYPTION};
use dpp::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Getters;
use dpp::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
use dpp::validation::SimpleConsensusValidationResult;
use dpp::version::PlatformVersion;
use drive::drive::identity::key::fetch::{
IdentityKeysRequest, KeyKindRequestType, KeyRequestType, OptionalSingleIdentityPublicKeyOutcome,
};
use drive::drive::Drive;
use drive::grovedb::TransactionArg;

pub(super) fn validate_identity_public_keys_contract_bounds_v1(
identity_id: Identifier,
identity_public_keys_with_witness: &[IdentityPublicKeyInCreation],
drive: &Drive,
epoch: &Epoch,
transaction: TransactionArg,
execution_context: &mut StateTransitionExecutionContext,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, Error> {
let mut per_key_results = Vec::with_capacity(identity_public_keys_with_witness.len());
for identity_public_key in identity_public_keys_with_witness {
per_key_results.push(validate_identity_public_key_contract_bounds_v1(
identity_id,
identity_public_key,
drive,
epoch,
transaction,
execution_context,
platform_version,
)?);
}
Ok(SimpleConsensusValidationResult::merge_many_errors(
per_key_results,
))
}

fn validate_identity_public_key_contract_bounds_v1(
identity_id: Identifier,
identity_public_key_in_creation: &IdentityPublicKeyInCreation,
drive: &Drive,
epoch: &Epoch,
transaction: TransactionArg,
execution_context: &mut StateTransitionExecutionContext,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, Error> {
let purpose = identity_public_key_in_creation.purpose();
let Some(contract_bounds) = identity_public_key_in_creation.contract_bounds() else {
return Ok(SimpleConsensusValidationResult::new());
};

// Resolve the bounded contract. System contracts come from the in-memory cache
// (no fee); user contracts come from grovedb (billed); a missing contract still
// costs the grovedb lookup that proved its absence (also billed).
let contract_id = match contract_bounds {
ContractBounds::SingleContract { id } => *id,
ContractBounds::SingleContractDocumentType { id, .. } => *id,
};
let outcome = drive.get_system_or_user_contract_with_fee(
contract_id.to_buffer(),
epoch,
transaction,
platform_version,
)?;
if let Some(fee) = outcome.fee() {
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee.clone()));
}
let Some(contract) = outcome.contract() else {
return Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::BasicError(BasicError::DataContractNotPresentError(
DataContractNotPresentError::new(contract_id),
)),
));
};

match contract_bounds {
ContractBounds::SingleContract { .. } => {
let requirements_for_purpose = match purpose {
ENCRYPTION => contract.config().requires_identity_encryption_bounded_key(),
DECRYPTION => contract.config().requires_identity_decryption_bounded_key(),
purpose => {
return Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::BasicError(
BasicError::InvalidKeyPurposeForContractBoundsError(
InvalidKeyPurposeForContractBoundsError::new(
purpose,
vec![ENCRYPTION, DECRYPTION],
),
),
),
));
}
};
let Some(requirements) = requirements_for_purpose else {
return Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::BasicError(BasicError::DataContractBoundsNotPresentError(
DataContractBoundsNotPresentError::new(contract_id),
)),
));
};
check_unique_bound_key(
identity_id,
identity_public_key_in_creation,
contract_id,
purpose,
requirements,
KeyRequestType::ContractBoundKey(
contract_id.to_buffer(),
purpose,
KeyKindRequestType::CurrentKeyOfKindRequest,
),
drive,
epoch,
transaction,
execution_context,
platform_version,
)
}
ContractBounds::SingleContractDocumentType {
document_type_name, ..
} => {
let Some(document_type) =
contract.document_type_optional_for_name(document_type_name.as_str())
else {
return Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::BasicError(BasicError::InvalidDocumentTypeError(
InvalidDocumentTypeError::new(document_type_name.clone(), contract_id),
)),
));
};
let requirements_for_purpose = match purpose {
ENCRYPTION => document_type.requires_identity_encryption_bounded_key(),
// v1 fix: v0 mistakenly called `requires_identity_encryption_bounded_key()` here.
DECRYPTION => document_type.requires_identity_decryption_bounded_key(),
purpose => {
return Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::BasicError(
BasicError::InvalidKeyPurposeForContractBoundsError(
InvalidKeyPurposeForContractBoundsError::new(
purpose,
vec![ENCRYPTION, DECRYPTION],
),
),
),
));
}
};
let Some(requirements) = requirements_for_purpose else {
return Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::BasicError(BasicError::DataContractBoundsNotPresentError(
DataContractBoundsNotPresentError::new(contract_id),
)),
));
};
check_unique_bound_key(
identity_id,
identity_public_key_in_creation,
contract_id,
purpose,
requirements,
KeyRequestType::ContractDocumentTypeBoundKey(
contract_id.to_buffer(),
document_type_name.clone(),
purpose,
KeyKindRequestType::CurrentKeyOfKindRequest,
),
drive,
epoch,
transaction,
execution_context,
platform_version,
)
}
}
}

#[allow(clippy::too_many_arguments)]
fn check_unique_bound_key(
identity_id: Identifier,
identity_public_key_in_creation: &IdentityPublicKeyInCreation,
contract_id: Identifier,
purpose: Purpose,
requirements: StorageKeyRequirements,
request_type: KeyRequestType,
drive: &Drive,
epoch: &Epoch,
transaction: TransactionArg,
execution_context: &mut StateTransitionExecutionContext,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, Error> {
match requirements {
StorageKeyRequirements::Unique => {
let key_request = IdentityKeysRequest {
identity_id: identity_id.to_buffer(),
request_type,
limit: None,
offset: None,
};
let (maybe_conflicting_key, fee) = drive
.fetch_identity_keys_with_costs::<OptionalSingleIdentityPublicKeyOutcome>(
key_request,
epoch,
transaction,
platform_version,
)?;
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee));
if let Some(conflicting_key) = maybe_conflicting_key {
Ok(SimpleConsensusValidationResult::new_with_error(
ConsensusError::StateError(
StateError::IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError(
IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError::new(
identity_id,
contract_id,
purpose,
identity_public_key_in_creation.id(),
conflicting_key.id(),
),
),
),
))
} else {
Ok(SimpleConsensusValidationResult::new())
}
}
StorageKeyRequirements::Multiple | StorageKeyRequirements::MultipleReferenceToLatest => {
Ok(SimpleConsensusValidationResult::new())
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::Error;

use crate::platform_types::platform::PlatformRef;
use crate::platform_types::platform_state::PlatformStateV0Methods;
use crate::rpc::core::CoreRPCLike;

use dpp::prelude::ConsensusValidationResult;
Expand Down Expand Up @@ -100,6 +101,7 @@ impl IdentityUpdateStateTransitionStateValidationV0 for IdentityUpdateTransition
self.identity_id(),
self.public_keys_to_add(),
drive,
platform.state.last_committed_block_epoch_ref(),
tx,
&mut state_transition_execution_context,
platform_version,
Expand Down
26 changes: 26 additions & 0 deletions packages/rs-drive/src/cache/system_contracts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::Error;
use arc_swap::{ArcSwap, Guard};
use dpp::data_contract::DataContract;
use dpp::prelude::Identifier;
use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
use platform_version::version::{PlatformVersion, ProtocolVersion};
use std::sync::Arc;
Expand Down Expand Up @@ -172,4 +173,29 @@ impl SystemDataContracts {
pub fn load_keyword_search(&self) -> Guard<Arc<DataContract>> {
self.keyword_search.load()
}

/// Returns the cached system contract whose deterministic identifier matches `id`,
/// if any. Returns `None` for user contracts and for any system contract whose
/// definition isn't held in this in-memory cache (e.g. `WalletUtils`, which lives
/// only in grovedb).
pub fn find_by_id(&self, id: Identifier) -> Option<Arc<DataContract>> {
// Compare against each cached system contract's static `id_bytes`. The match
// is `O(n)` over a small fixed set of variants — cheaper than building a map.
let active = if id == SystemDataContract::Withdrawals.id() {
&self.withdrawals
} else if id == SystemDataContract::MasternodeRewards.id() {
&self.masternode_reward_shares
} else if id == SystemDataContract::DPNS.id() {
&self.dpns
} else if id == SystemDataContract::Dashpay.id() {
&self.dashpay
} else if id == SystemDataContract::TokenHistory.id() {
&self.token_history
} else if id == SystemDataContract::KeywordSearch.id() {
&self.keyword_search
} else {
return None;
};
Some(Arc::clone(&active.load()))
}
}
Loading
Loading