Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
68523e5
fix(platform-wallet): never sign credit or identity-update transition…
lklimek Oct 26, 2026
0569898
feat(dpp): add SecurityLevel::accepted_for_requirement
lklimek Oct 26, 2026
956631a
feat(dpp): check contract bounds and the non-batch rule before a tran…
lklimek Oct 26, 2026
7066da0
feat(platform-wallet): expose protocol-14-aware signing-key selection
lklimek Oct 26, 2026
ed4fb7d
fix(platform-wallet): choose protocol-14-aware keys for DPNS, contact…
lklimek Oct 26, 2026
7998aff
fix(platform-wallet): choose protocol-14-aware keys for token, contra…
lklimek Oct 26, 2026
68d0ed0
feat(platform-wallet): make select_own_encryption_key public
lklimek Oct 26, 2026
4e57a67
feat(platform-wallet): expose SigningScope::bounds_check
lklimek Oct 26, 2026
eaecaa5
style(platform-wallet): import key-selection helpers at the top
lklimek Oct 26, 2026
6bb7927
Merge branch 'v5.1-dev' into feat/platform-wallet-public-signing-key-…
lklimek Oct 26, 2026
f7e4f79
Merge branch 'v5.1-dev' into feat/platform-wallet-public-signing-key-…
lklimek Oct 27, 2026
949f6f9
Revert "feat(platform-wallet): make select_own_encryption_key public"
lklimek Oct 27, 2026
d32f732
refactor(platform-wallet): export key-selection helpers from the crat…
lklimek Oct 27, 2026
c0eccaf
refactor(platform-wallet): take the non-batch bounds rule from dpp
lklimek Oct 27, 2026
1f17f83
feat(platform-wallet): report a missing signing key as NoUsableSignin…
lklimek Oct 27, 2026
3e476c7
fix(platform-wallet): check an explicit DPNS registration key before …
lklimek Oct 27, 2026
68c996a
fix(platform-wallet): check explicit credit and token signing keys up…
lklimek Oct 27, 2026
9122d9c
refactor(platform-wallet): let check_signing_key take every accepted …
lklimek Oct 27, 2026
e8c391c
fix(platform-wallet): accept a TRANSFER key for an explicit token tra…
lklimek Oct 27, 2026
f5d1518
fix(platform-wallet): accept every key type Platform verifies for an …
lklimek Oct 27, 2026
cd04dc6
feat(platform-wallet): name the accepted key types in NoUsableSigningKey
lklimek Oct 27, 2026
dacc85d
style(platform-wallet): tidy the key-check call sites and their tests
lklimek Oct 27, 2026
f34643f
style(platform-wallet): one constant for the key types the wallet pic…
lklimek Oct 27, 2026
d78d7be
fix(platform-wallet): treat a key expiry as unknown when the clock ca…
lklimek Oct 27, 2026
dd905ac
fix(platform-wallet): check token keys against the token, not the who…
lklimek Oct 27, 2026
a41a26b
fix(platform-wallet): derive the DPNS registration key level from the…
lklimek Oct 27, 2026
eb09e53
fix(platform-wallet): sign a withdrawal to a chosen address only with…
lklimek Oct 27, 2026
b24cba2
fix(platform-wallet): pick an automatic credit key only of a type Dri…
lklimek Oct 27, 2026
96f27ac
docs(platform-wallet): note the credit-key exception on the key-type …
lklimek Oct 27, 2026
ab842ef
fix(platform-wallet): refuse a wrong-purpose key with Drive's consens…
lklimek Oct 27, 2026
6ffd86a
fix(platform-wallet): sign credit transfers and withdrawals only with…
lklimek Oct 27, 2026
dd67de2
fix(platform-wallet): derive the contact-request key level from the D…
lklimek Oct 27, 2026
68769e0
fix(platform-wallet): refuse explicit keys with Drive's consensus err…
lklimek Oct 27, 2026
a2b3c08
test(platform-wallet): pin the key-type-before-purpose check order
lklimek Oct 27, 2026
af044e6
feat(platform-wallet): export the signing key-type sets with the key …
lklimek Oct 27, 2026
45567c9
fix(platform-wallet): check contract bounds before expiry, as Drive does
lklimek Oct 27, 2026
227ec7b
fix(platform-wallet): check an explicit document key before fetching …
lklimek Oct 27, 2026
5a7f518
fix(platform-wallet): check a masternode withdrawal key like an ident…
lklimek Oct 27, 2026
4ac66a4
fix(platform-wallet): require a create key to cover the types the cre…
lklimek Oct 27, 2026
59444a0
refactor(dpp): ask can_sign_non_batch in the pre-sign bounds check
lklimek Oct 27, 2026
f681ab7
test(platform-wallet,dpp): pin the expected outcomes instead of compa…
lklimek Oct 27, 2026
93bf916
Merge commit '13c8b0a40ffce2fabeb3c4ffaf22237635a17b6f' into pr5588-r7
lklimek Oct 28, 2026
3cc2b13
fix(platform-wallet,sdk): judge DPNS signing keys by the contract the…
lklimek Oct 28, 2026
ccb3d7a
fix(platform-wallet): sign with the weakest security level the requir…
lklimek Oct 28, 2026
1e4f3d2
fix(platform-wallet): pick ECDSA_HASH160 signing keys, preferring DIP…
lklimek Oct 28, 2026
e958003
fix(platform-wallet): sign a contract create with a HIGH key
lklimek Oct 28, 2026
747d5cd
fix(platform-wallet,sdk): judge contact-request signing keys by the c…
lklimek Oct 28, 2026
d098bdb
revert(platform-wallet): leave the profile and contactInfo key-type l…
lklimek Oct 28, 2026
44f2e40
test(platform-wallet): pin the DIP-11-first order of automatic credit…
lklimek Oct 28, 2026
155f8f1
fix(sdk): prepare contact requests against the DashPay contract the n…
lklimek Oct 28, 2026
51f35ad
fix(platform-wallet): let automatic selection sign a lone token trans…
lklimek Oct 28, 2026
e18ff2f
fix(sdk): keep the name and preorder salt out of a prepared DPNS regi…
lklimek Oct 28, 2026
3d44b9c
fix(platform-wallet): fall back to a TRANSFER token key only when no …
lklimek Oct 28, 2026
cafa0b6
refactor(dpp): expose BatchTransitionV1::signing_key_purposes as the …
lklimek Oct 28, 2026
129e5fc
refactor(platform-wallet): ask dpp which purposes may sign each token…
lklimek Oct 28, 2026
8e1173b
refactor(sdk): resolve the contact-request DashPay contract the estab…
lklimek Oct 29, 2026
1c34842
Merge remote-tracking branch 'origin/v5.1-dev' into feat/platform-wal…
lklimek Oct 30, 2026
cdb38c5
Merge remote-tracking branch 'origin/v5.1-dev' into feat/platform-wal…
lklimek Oct 30, 2026
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
155 changes: 127 additions & 28 deletions packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#[cfg(feature = "state-transitions")]
use crate::contract_group::ContractGroupMember;
#[cfg(feature = "state-transitions")]
use crate::data_contract::TokenContractPosition;
use crate::identifier::Identifier;
use crate::identity::identity_public_key::contract_bounds::ContractBounds::{
ContractGroup, SingleContract, SingleContractDocumentType,
Expand Down Expand Up @@ -82,6 +84,19 @@ pub enum BatchedTransitionBoundsCheck {
},
}

/// The part of one contract a transition acts on, known before the transition is built.
#[cfg(feature = "state-transitions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContractBoundsScope<'a> {
/// The whole contract: every document type and every token, as in a flow that writes more
/// than one document type.
Contract,
/// One document type of the contract.
DocumentType(&'a str),
/// One token of the contract, by its position in the contract.
Token(TokenContractPosition),
}

impl ContractBounds {
/// Creates a new contract bounds for the key
pub fn new_from_type(
Expand Down Expand Up @@ -184,43 +199,56 @@ impl ContractBounds {
pub fn check_batched_transition(
&self,
transition: BatchedTransitionRef<'_>,
) -> BatchedTransitionBoundsCheck {
match transition {
BatchedTransitionRef::Document(document) => self.check_scope(
document.data_contract_id(),
ContractBoundsScope::DocumentType(document.document_type_name()),
),
BatchedTransitionRef::Token(token) => self.check_scope(
token.data_contract_id(),
ContractBoundsScope::Token(token.base().token_contract_position()),
),
}
}

/// What authorizing an action on `scope` of `contract_id` with a key carrying these bounds
/// needs: [`Self::check_batched_transition`] for a transition not built yet, so a client
/// can choose a key before it builds and signs.
#[cfg(feature = "state-transitions")]
pub fn check_scope(
&self,
contract_id: Identifier,
scope: ContractBoundsScope<'_>,
) -> BatchedTransitionBoundsCheck {
use BatchedTransitionBoundsCheck::{Allowed, Denied, RequiresContractGroupMembership};
let allowed_if = |inside: bool| if inside { Allowed } else { Denied };
match (self, transition) {
(SingleContract { id }, BatchedTransitionRef::Document(document)) => {
allowed_if(document.data_contract_id() == *id)
}
(SingleContract { id }, BatchedTransitionRef::Token(token)) => {
allowed_if(token.data_contract_id() == *id)
}
match (self, scope) {
(SingleContract { id }, _) => allowed_if(contract_id == *id),
(
SingleContractDocumentType {
id,
document_type_name,
},
BatchedTransitionRef::Document(document),
ContractBoundsScope::DocumentType(scope_document_type_name),
) => allowed_if(
document.data_contract_id() == *id
&& document.document_type_name() == document_type_name.as_str(),
contract_id == *id && scope_document_type_name == document_type_name.as_str(),
),
(SingleContractDocumentType { .. }, BatchedTransitionRef::Token(_)) => Denied,
(ContractGroup { id }, BatchedTransitionRef::Document(document)) => {
RequiresContractGroupMembership {
contract_group_id: *id,
contract_id: document.data_contract_id(),
member: ContractGroupMember::DocumentType(
document.document_type_name().to_string(),
),
}
}
(ContractGroup { id }, BatchedTransitionRef::Token(token)) => {
RequiresContractGroupMembership {
contract_group_id: *id,
contract_id: token.data_contract_id(),
member: ContractGroupMember::Token(token.base().token_contract_position()),
}
}
(
SingleContractDocumentType { .. },
ContractBoundsScope::Contract | ContractBoundsScope::Token(_),
) => Denied,
(ContractGroup { id }, scope) => RequiresContractGroupMembership {
contract_group_id: *id,
contract_id,
member: match scope {
ContractBoundsScope::Contract => ContractGroupMember::Contract,
ContractBoundsScope::DocumentType(document_type_name) => {
ContractGroupMember::DocumentType(document_type_name.to_string())
}
ContractBoundsScope::Token(position) => ContractGroupMember::Token(position),
},
},
}
}
}
Expand Down Expand Up @@ -455,7 +483,7 @@ mod tests {

#[cfg(all(test, feature = "state-transitions"))]
mod batched_transition_tests {
use super::{BatchedTransitionBoundsCheck, ContractBounds};
use super::{BatchedTransitionBoundsCheck, ContractBounds, ContractBoundsScope};
use crate::contract_group::ContractGroupMember;
use crate::identifier::Identifier;
use crate::state_transition::batch_transition::batched_transition::{
Expand Down Expand Up @@ -570,6 +598,77 @@ mod batched_transition_tests {
}
}

#[test]
fn should_check_a_document_type_scope_against_the_bound_contract_and_type() {
let contract = Identifier::from([0; 32]);
let check = |bounds: ContractBounds| {
bounds.check_scope(contract, ContractBoundsScope::DocumentType("profile"))
};
for (bounds, expected) in [
(
ContractBounds::SingleContract { id: contract },
BatchedTransitionBoundsCheck::Allowed,
),
(
ContractBounds::SingleContractDocumentType {
id: contract,
document_type_name: "profile".to_string(),
},
BatchedTransitionBoundsCheck::Allowed,
),
(
ContractBounds::SingleContractDocumentType {
id: contract,
document_type_name: "contactInfo".to_string(),
},
BatchedTransitionBoundsCheck::Denied,
),
(
ContractBounds::SingleContractDocumentType {
id: Identifier::from([1; 32]),
document_type_name: "profile".to_string(),
},
BatchedTransitionBoundsCheck::Denied,
),
] {
assert_eq!(check(bounds.clone()), expected, "{bounds:?}");
}
}

#[test]
fn should_cover_the_whole_contract_only_with_a_whole_contract_bound() {
let contract = Identifier::from([0; 32]);
let check =
|bounds: ContractBounds| bounds.check_scope(contract, ContractBoundsScope::Contract);
assert_eq!(
check(ContractBounds::SingleContract { id: contract }),
BatchedTransitionBoundsCheck::Allowed
);
assert_eq!(
check(ContractBounds::SingleContract {
id: Identifier::from([1; 32])
}),
BatchedTransitionBoundsCheck::Denied
);
assert_eq!(
check(ContractBounds::SingleContractDocumentType {
id: contract,
document_type_name: String::new(),
}),
BatchedTransitionBoundsCheck::Denied,
"a document-type bound never covers the rest of the contract"
);
let group_id = Identifier::from([7; 32]);
assert_eq!(
check(ContractBounds::ContractGroup { id: group_id }),
BatchedTransitionBoundsCheck::RequiresContractGroupMembership {
contract_group_id: group_id,
contract_id: contract,
member: ContractGroupMember::Contract,
}
);
}

#[test]
fn should_defer_contract_group_bounds_to_state_with_the_member_to_look_up() {
let group_id = Identifier::from([7; 32]);
Expand Down
7 changes: 7 additions & 0 deletions packages/rs-dpp/src/identity/identity_public_key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ impl IdentityPublicKey {
self.security_level() == SecurityLevel::MASTER
}

/// Whether the key may sign a transition that is not a batch. A contract-bound
/// AUTHENTICATION key signs only batches inside its bounds; consensus refuses it anywhere
/// else.
pub fn can_sign_non_batch(&self) -> bool {
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved
self.purpose() != Purpose::AUTHENTICATION || self.contract_bounds().is_none()
}

/// Generates an identity public key with the maximum possible size based on the platform version.
///
/// This method constructs a key of the largest possible size for the given platform version.
Expand Down
87 changes: 87 additions & 0 deletions packages/rs-dpp/src/identity/identity_public_key/security_level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ impl SecurityLevel {
pub fn stronger_or_equal_security_than(self: SecurityLevel, rhs: SecurityLevel) -> bool {
(self as u8) <= (rhs as u8)
}

/// The levels a signing key may carry to meet `requirement`, strongest first: every level at
/// least as strong as `requirement`, except MASTER, which only a MASTER requirement admits.
/// This is the rule consensus applies to a batch (`combined_security_level_requirement`),
/// usable before a transition is built.
pub fn accepted_for_requirement(requirement: SecurityLevel) -> Vec<SecurityLevel> {
if requirement == Self::MASTER {
return vec![Self::MASTER];
}
[Self::CRITICAL, Self::HIGH, Self::MEDIUM]
.into_iter()
.filter(|level| level.stronger_or_equal_security_than(requirement))
.collect()
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved
}
}

impl std::fmt::Display for SecurityLevel {
Expand Down Expand Up @@ -274,3 +288,76 @@ mod tests {
}
}
}

#[cfg(all(test, feature = "state-transitions"))]
mod accepted_for_requirement_tests {
use super::SecurityLevel;
use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV0;
use crate::state_transition::batch_transition::batched_transition::{
BatchedTransition, DocumentTransition, TokenTransition,
};
use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0;
use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionV1};
use crate::ProtocolError;
use platform_value::Identifier;

type RequirementFn = fn(Identifier, String) -> Result<SecurityLevel, ProtocolError>;

fn batch(member: BatchedTransition) -> BatchTransition {
BatchTransitionV1 {
transitions: vec![member],
..Default::default()
}
.into()
}

#[test]
fn should_accept_what_consensus_accepts_for_a_document_batch() {
for requirement in SecurityLevel::full_range() {
let document = batch(BatchedTransition::Document(DocumentTransition::Delete(
DocumentDeleteTransitionV0::default().into(),
)));
let consensus = document
.combined_security_level_requirement(Some(|_: Identifier, _: String| {
Ok(requirement)
}))
.unwrap();
assert_eq!(
SecurityLevel::accepted_for_requirement(requirement),
consensus,
"{requirement:?}"
);
}
}

#[test]
fn should_accept_what_consensus_accepts_for_a_token_batch() {
let token = batch(BatchedTransition::Token(TokenTransition::Burn(
Default::default(),
)));
assert_eq!(
SecurityLevel::accepted_for_requirement(SecurityLevel::CRITICAL),
token
.combined_security_level_requirement(None::<RequirementFn>)
.unwrap()
);
}

#[test]
fn should_list_stronger_levels_and_admit_master_only_for_master() {
use SecurityLevel::{CRITICAL, HIGH, MASTER, MEDIUM};
assert_eq!(SecurityLevel::accepted_for_requirement(MASTER), [MASTER]);
assert_eq!(
SecurityLevel::accepted_for_requirement(CRITICAL),
[CRITICAL]
);
assert_eq!(
SecurityLevel::accepted_for_requirement(HIGH),
[CRITICAL, HIGH]
);
assert_eq!(
SecurityLevel::accepted_for_requirement(MEDIUM),
[CRITICAL, HIGH, MEDIUM]
);
}
}
62 changes: 56 additions & 6 deletions packages/rs-dpp/src/state_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2027,15 +2027,15 @@ impl StateTransition {
&self,
identity_public_key: &IdentityPublicKey,
) -> Result<(), ProtocolError> {
if identity_public_key.purpose() != Purpose::AUTHENTICATION {
return Ok(());
}
let Some(bounds) = identity_public_key.contract_bounds() else {
return Ok(());
};
match self {
StateTransition::Batch(batch) => {
use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0;
if identity_public_key.purpose() != Purpose::AUTHENTICATION {
return Ok(());
}
let Some(bounds) = identity_public_key.contract_bounds() else {
return Ok(());
};
if batch.transitions_iter().all(|member| {
bounds.check_batched_transition(member) != BatchedTransitionBoundsCheck::Denied
}) {
Expand All @@ -2046,6 +2046,7 @@ impl StateTransition {
)))
}
}
_ if identity_public_key.can_sign_non_batch() => Ok(()),
_ => Err(ProtocolError::ConsensusError(Box::new(
ContractBoundedKeyNonBatchError::new(identity_public_key.id()).into(),
))),
Expand Down Expand Up @@ -3161,6 +3162,55 @@ mod tests {
))
}

#[cfg(feature = "state-transition-signing")]
#[test]
fn should_refuse_only_a_contract_bound_authentication_key_for_a_non_batch_transition() {
use crate::consensus::signature::ContractBoundedKeyNonBatchError;
use crate::identity::contract_bounds::ContractBounds;
use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
use strum::IntoEnumIterator;

let every_bounds = [
None,
Some(ContractBounds::SingleContract {
id: Identifier::from([2; 32]),
}),
Some(ContractBounds::SingleContractDocumentType {
id: Identifier::from([2; 32]),
document_type_name: "preorder".to_string(),
}),
Some(ContractBounds::ContractGroup {
id: Identifier::from([3; 32]),
}),
];
let transfer = sample_transfer_st();
for purpose in Purpose::iter() {
for contract_bounds in &every_bounds {
let key: IdentityPublicKey = IdentityPublicKeyV0 {
id: 7,
purpose,
contract_bounds: contract_bounds.clone(),
..Default::default()
}
.into();
let expected = purpose != Purpose::AUTHENTICATION || contract_bounds.is_none();
assert_eq!(
key.can_sign_non_batch(),
expected,
"{purpose:?} {contract_bounds:?}"
);
match transfer.verify_identity_key_bounds(&key) {
Ok(()) => assert!(expected, "{purpose:?} {contract_bounds:?}"),
Err(ProtocolError::ConsensusError(error)) => {
assert!(!expected, "{purpose:?} {contract_bounds:?}");
assert_eq!(*error, ContractBoundedKeyNonBatchError::new(7).into());
}
Err(other) => panic!("{purpose:?} {contract_bounds:?}: {other:?}"),
}
}
}
}

#[cfg(all(feature = "state-transition-signing", feature = "bls-signatures"))]
#[test]
fn should_enforce_contract_bounds_before_private_key_signing() {
Expand Down
Loading
Loading