Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5fa4714
test(sdk): open an entry with the first of a wallet's prepared keys
QuantumExplorer Nov 8, 2026
1549627
feat(sdk): send, scan and read private contact requests
QuantumExplorer Nov 8, 2026
a6ca844
feat(sdk): name the sender key of a recovered private contact request
QuantumExplorer Nov 8, 2026
c3f1b0d
feat(sdk): sign private contact requests with the sender's signing key
QuantumExplorer Nov 8, 2026
6db333e
fix(sdk): bind the recipient key into a private request's signature
QuantumExplorer Nov 8, 2026
3d510b8
fix(sdk): export private_contact_request_signing_keys
QuantumExplorer Nov 8, 2026
d4efa2e
fix(sdk): validate private requests with dpp and check proved identit…
QuantumExplorer Nov 8, 2026
473a440
fix(sdk): refuse expired signing keys and refresh a DashPay without t…
QuantumExplorer Nov 8, 2026
4a4826a
fix(sdk): keep encryption keys inside their contract bounds and check…
QuantumExplorer Nov 8, 2026
785e493
fix(sdk): give an empty context provider the DashPay it fetches, and …
QuantumExplorer Nov 8, 2026
b3a0ad3
fix(wasm-dpp2): key the constructor's tokens by position as text, and…
QuantumExplorer Nov 8, 2026
f5dde30
fix(wasm-dpp2): refuse tokens for a contract that cannot hold them
QuantumExplorer Nov 8, 2026
410b38c
fix(sdk): refuse a scan of another store as DashPay private contact r…
QuantumExplorer Nov 8, 2026
ab68754
refactor(dpp): give the document signing levels of a security require…
QuantumExplorer Nov 8, 2026
1bd3788
fix(sdk): read a private contact request only through a key it could …
QuantumExplorer Nov 8, 2026
609f529
fix(sdk): wipe the outgoing viewing key a private contact request is …
QuantumExplorer Nov 8, 2026
5839100
fix(sdk): give a private page reader's context provider the contract …
QuantumExplorer Nov 8, 2026
be7fba8
feat(sdk): stream a private document store's pages, many fetched at once
QuantumExplorer Nov 8, 2026
b8e41ab
fix(sdk): refuse a private document read below protocol version 15 as…
QuantumExplorer Nov 8, 2026
d91c1c6
fix(sdk): name the SDK's protocol version when building a private doc…
QuantumExplorer Nov 8, 2026
0e3f521
fix(wasm-dpp2): refuse tokens set on a data contract that holds none
QuantumExplorer Nov 8, 2026
3f546c0
docs(dpp): say at the batch signing levels why the in-place change ke…
QuantumExplorer Nov 8, 2026
e710dd7
docs(sdk): say what the private contact request parser and signing le…
QuantumExplorer Nov 8, 2026
454f25c
test(sdk): check a recipient key holding another point is refused by …
QuantumExplorer Nov 8, 2026
3b68903
test(sdk): pin a private page stream's order and its cursor under a g…
QuantumExplorer Nov 8, 2026
898e8fc
fix(sdk): keep the store a private document scan was read from, and r…
QuantumExplorer Nov 8, 2026
91d8782
fix(sdk): send a private document transition only once the network is…
QuantumExplorer Nov 9, 2026
19ab417
fix(sdk): keep each outgoing viewing key in place as scan senders are…
QuantumExplorer Nov 9, 2026
59e047a
fix(sdk): learn a private store's chunk size only from a page that en…
QuantumExplorer Nov 9, 2026
314d957
fix(sdk): refuse a private contact request scan of a contract whose t…
QuantumExplorer Nov 9, 2026
e11ef7c
fix(sdk): keep a newer contract over a registered older one
QuantumExplorer Nov 9, 2026
efa0fc8
fix(sdk): complete a private store chunk an older snapshot ended part…
QuantumExplorer Nov 9, 2026
baa3092
fix(sdk): gate every type 29 broadcast and refuse it with the structu…
QuantumExplorer Nov 9, 2026
40e4f57
fix(sdk): keep a newer contract in the gRPC context provider, and say…
QuantumExplorer Nov 9, 2026
f7fcc44
fix(sdk): report an identity proved before its entry as a retryable s…
QuantumExplorer Nov 9, 2026
b065ca3
fix(sdk): read a private contact request under an eligible alias of i…
QuantumExplorer Nov 9, 2026
4712db7
fix(sdk): fetch again a private page an older snapshot left empty, th…
QuantumExplorer Nov 9, 2026
8558e83
feat(sdk): schedule a private store's page walk from the chunk size i…
QuantumExplorer Nov 9, 2026
4386ec0
docs(sdk): say that a private request's key expiry is the identity's …
QuantumExplorer Nov 9, 2026
f9d669f
fix(sdk-trusted-context-provider): keep a newer known contract over a…
QuantumExplorer Nov 9, 2026
311ae62
fix(sdk): keep a failed refresh as the cause of a private document se…
QuantumExplorer Nov 9, 2026
4e75e27
fix(sdk): read a private page against a newer contract with the type …
QuantumExplorer Nov 9, 2026
235a4f8
fix(sdk): take the network as proven below a version only on a curren…
QuantumExplorer Nov 9, 2026
ecf621d
docs(wasm-sdk): say that a registration keeps a held contract of a hi…
QuantumExplorer Nov 9, 2026
0dc274c
fix(sdk): ask the protocol gate before refusing a private document send
QuantumExplorer Nov 9, 2026
9b8589a
test(drive-abci): export a protocol 15 epoch probe for the SDK's gate…
QuantumExplorer Nov 9, 2026
50658b5
fix(sdk): establish protocol 15 through the gate for DashPay's privat…
QuantumExplorer Nov 9, 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
6 changes: 6 additions & 0 deletions packages/rs-context-provider/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ pub trait ContextProvider: Send + Sync {
/// Providers that maintain a writable known-contracts cache (e.g. the
/// mobile `TrustedHttpContextProvider`) override this; the default is a
/// no-op for providers that fetch on demand or don't cache.
///
/// A provider that overrides it keeps a held contract of a higher version
/// than `contract`, deciding atomically under its own cache lock: a
/// registration that waited on a fetch must not replace a newer contract
/// another operation registered meanwhile, which a proof of that newer
/// version would then fail to verify against.
fn register_data_contract(&self, _contract: Arc<DataContract>) {}

/// Fetches the token configuration for a specified token ID.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ pub enum BatchedTransitionBoundsCheck {
},
}

/// Whether a key carrying contract bounds may be used with the documents of one document type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocumentTypeBoundsCheck {
/// The document type lies inside the bounds.
Allowed,
/// The document type lies outside the bounds.
Denied,
/// The bounds name a contract group. The type lies inside them when its contract holds it,
/// or is a whole-contract member, of the group: only state can answer that.
RequiresContractGroupMembership {
/// The group the bounds name.
contract_group_id: Identifier,
},
}

impl ContractBounds {
/// Creates a new contract bounds for the key
pub fn new_from_type(
Expand Down Expand Up @@ -175,6 +190,33 @@ impl ContractBounds {
}
}

/// Whether a key carrying these bounds may be used with the documents of the type
/// `document_type_name` of the contract `contract_id`. Clients scope their ENCRYPTION and
/// DECRYPTION keys with this, as the bounds ask. A contract group bound is answered by the
/// group's members, which only state holds: a client that does not read them treats the
/// type as outside the bounds.
pub fn check_document_type(
&self,
contract_id: &Identifier,
document_type_name: &str,
) -> DocumentTypeBoundsCheck {
match self {
SingleContract { id } if id == contract_id => DocumentTypeBoundsCheck::Allowed,
SingleContractDocumentType {
id,
document_type_name: bound_document_type_name,
} if id == contract_id && bound_document_type_name == document_type_name => {
DocumentTypeBoundsCheck::Allowed
}
SingleContract { .. } | SingleContractDocumentType { .. } => {
DocumentTypeBoundsCheck::Denied
}
ContractGroup { id } => DocumentTypeBoundsCheck::RequiresContractGroupMembership {
contract_group_id: *id,
},
}
}

/// What authorizing `transition` with a key carrying these bounds needs. Consensus uses this
/// to authorize a batch signed by a contract-bound AUTHENTICATION key. Token operations are
/// contract-wide, so a document-type bound never covers them. A contract group bound is
Expand Down Expand Up @@ -376,6 +418,85 @@ mod core_tests {
}
}

#[cfg(test)]
mod document_type_tests {
use super::{ContractBounds, DocumentTypeBoundsCheck};
use crate::identifier::Identifier;

#[test]
fn should_allow_only_the_bound_contract_or_document_type() {
let contract_id = Identifier::from([1; 32]);
let other_id = Identifier::from([2; 32]);
let contract = ContractBounds::SingleContract { id: contract_id };
let document_type = ContractBounds::SingleContractDocumentType {
id: contract_id,
document_type_name: "note".to_string(),
};
for (case, bounds, checked_contract, checked_type, expected) in [
(
"a contract bound, a type of it",
&contract,
contract_id,
"note",
DocumentTypeBoundsCheck::Allowed,
),
(
"a contract bound, another type of it",
&contract,
contract_id,
"other",
DocumentTypeBoundsCheck::Allowed,
),
(
"a contract bound, another contract",
&contract,
other_id,
"note",
DocumentTypeBoundsCheck::Denied,
),
(
"a type bound, that type",
&document_type,
contract_id,
"note",
DocumentTypeBoundsCheck::Allowed,
),
(
"a type bound, another type",
&document_type,
contract_id,
"other",
DocumentTypeBoundsCheck::Denied,
),
(
"a type bound, that type's name in another contract",
&document_type,
other_id,
"note",
DocumentTypeBoundsCheck::Denied,
),
] {
assert_eq!(
bounds.check_document_type(&checked_contract, checked_type),
expected,
"{case}"
);
}
}

#[test]
fn should_leave_a_contract_group_bound_to_its_members() {
let contract_group_id = Identifier::from([3; 32]);
let bounds = ContractBounds::ContractGroup {
id: contract_group_id,
};
assert_eq!(
bounds.check_document_type(&Identifier::from([1; 32]), "note"),
DocumentTypeBoundsCheck::RequiresContractGroupMembership { contract_group_id }
);
}
}

#[cfg(all(test, feature = "json-conversion"))]
mod tests {
use super::*;
Expand Down
43 changes: 43 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,21 @@ impl SecurityLevel {
pub fn stronger_or_equal_security_than(self: SecurityLevel, rhs: SecurityLevel) -> bool {
(self as u8) <= (rhs as u8)
}

/// The security levels a key may sign a document at when the document's type requires
/// `self` (its `security_level_requirement`): `MASTER` alone when the requirement is
/// `MASTER`, otherwise every level from `CRITICAL` to the requirement, strongest first. A
/// batch takes the levels of the strongest requirement among its documents
/// (`DocumentsBatchTransitionMethodsV0::combined_security_level_requirement`).
pub fn document_signing_levels(self) -> Vec<SecurityLevel> {
if self == Self::MASTER {
return vec![Self::MASTER];
}
Self::full_range()
.into_iter()
.filter(|level| *level != Self::MASTER && (*level as u8) <= (self as u8))
.collect()
}
}

impl std::fmt::Display for SecurityLevel {
Expand All @@ -117,6 +132,34 @@ impl std::fmt::Display for SecurityLevel {
mod tests {
use super::*;

/// Each requirement admits `MASTER` alone, or `CRITICAL` to itself, as a document batch
/// has always computed it.
#[test]
fn should_give_the_levels_each_document_requirement_admits() {
use SecurityLevel::*;
assert_eq!(MASTER.document_signing_levels(), vec![MASTER]);
assert_eq!(CRITICAL.document_signing_levels(), vec![CRITICAL]);
assert_eq!(HIGH.document_signing_levels(), vec![CRITICAL, HIGH]);
assert_eq!(
MEDIUM.document_signing_levels(),
vec![CRITICAL, HIGH, MEDIUM]
);
for requirement in SecurityLevel::full_range() {
let range: Vec<SecurityLevel> = if requirement == MASTER {
vec![MASTER]
} else {
(CRITICAL as u8..=requirement as u8)
.map(|level| SecurityLevel::try_from(level).expect("a security level"))
.collect()
};
assert_eq!(
requirement.document_signing_levels(),
range,
"{requirement}"
);
}
}

// -- TryFrom<u8> valid --
#[test]
fn test_security_level_try_from_u8_all_valid() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use crate::ProtocolError;
use platform_value::Identifier;
#[cfg(feature = "state-transition-signing")]
use platform_version::version::PlatformVersion;
use std::convert::TryFrom;
use crate::state_transition::batch_transition::batched_transition::{BatchedTransition, BatchedTransitionRef};
#[cfg(feature = "state-transition-signing")]
use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions;
Expand Down Expand Up @@ -165,14 +164,12 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0
}
};

Ok(if highest_security_level == SecurityLevel::MASTER {
vec![SecurityLevel::MASTER]
} else {
// this might seem wrong until you realize that master is 0, critical 1, etc
(SecurityLevel::CRITICAL as u8..=highest_security_level as u8)
.map(|security_level| SecurityLevel::try_from(security_level).unwrap())
.collect()
})
// Edited in place in a shipped generation: `document_signing_levels` returns what the
// code it replaced did for every requirement (MASTER alone for MASTER, otherwise
// CRITICAL through the requirement, strongest first), without its `unwrap`. Its tests
// and the batch tests pin each output, so no protocol version that signs or checks a
// batch through this method can see another result.
Ok(highest_security_level.document_signing_levels())
Comment thread
QuantumExplorer marked this conversation as resolved.
Resolved
}

fn set_transitions(&mut self, transitions: Vec<BatchedTransition>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,69 @@ mod batch_transition_tests {
assert_eq!(purposes, vec![Purpose::AUTHENTICATION]);
}

// -----------------------------------------------------------------------
// combined_security_level_requirement
// -----------------------------------------------------------------------

/// The levels a batch may be signed at, pinned for each document requirement, for the
/// strongest of two requirements, for a token transition and for an empty batch: they
/// come from `SecurityLevel::document_signing_levels` and must not change.
#[test]
fn should_keep_the_levels_a_batch_may_be_signed_at() {
use SecurityLevel::*;
let documents = make_batch_v1(vec![
BatchedTransition::Document(make_delete_transition(1)),
BatchedTransition::Document(make_delete_transition(2)),
]);
for (requirement, expected) in [
(MASTER, vec![MASTER]),
(CRITICAL, vec![CRITICAL]),
(HIGH, vec![CRITICAL, HIGH]),
(MEDIUM, vec![CRITICAL, HIGH, MEDIUM]),
] {
let levels = documents
.combined_security_level_requirement(Some(
move |_: Identifier, _: String| -> Result<SecurityLevel, ProtocolError> {
Ok(requirement)
},
))
.expect("the levels");
assert_eq!(levels, expected, "{requirement}");
}

// The strongest requirement among the batch's documents wins.
let calls = std::cell::Cell::new(0);
let mixed = documents
.combined_security_level_requirement(Some(
|_: Identifier, _: String| -> Result<SecurityLevel, ProtocolError> {
calls.set(calls.get() + 1);
Ok(if calls.get() == 1 { MEDIUM } else { HIGH })
},
))
.expect("the levels");
assert_eq!(mixed, vec![CRITICAL, HIGH]);

let token = make_batch_v1(vec![BatchedTransition::Token(make_token_burn_transition(
1, 10,
))]);
assert_eq!(
token
.combined_security_level_requirement(
None::<fn(Identifier, String) -> Result<SecurityLevel, ProtocolError>>
)
.expect("the levels"),
vec![CRITICAL]
);
assert_eq!(
make_batch_v1(vec![])
.combined_security_level_requirement(
None::<fn(Identifier, String) -> Result<SecurityLevel, ProtocolError>>
)
.expect("the levels"),
vec![CRITICAL, HIGH, MEDIUM]
);
}

// -----------------------------------------------------------------------
// get_security_level_requirement function
// -----------------------------------------------------------------------
Expand Down
11 changes: 6 additions & 5 deletions packages/rs-drive-abci/tests/protocol_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,10 @@ fn should_export_protocol_gate_sdk_fixtures() {
Provider.get_quorum_public_key(106, [2; 32], 0).unwrap(),
)
.unwrap();
// The versions on either side of protocol 14, fixed so that re-running on a later
// branch rewrites the same files the SDK tests read.
for signed in [13, 14] {
// The versions on either side of protocol 14, and protocol 15, which the private
// document requests need, fixed so that re-running on a later branch rewrites the same
// files the SDK tests read.
for signed in [13, 14, 15] {
let answer = Node::new(signed).epoch_probe();
assert_eq!(verified_epoch_probe(answer.clone(), signed), signed);
std::fs::write(
Expand Down Expand Up @@ -455,11 +456,11 @@ fn should_export_protocol_gate_sdk_fixtures() {
#[test]
fn should_keep_the_recorded_protocol_gate_vectors_verifiable() {
let read = |name: &str| std::fs::read(directory().join(name)).unwrap();
for signed in [13, 14] {
for signed in [13, 14, 15] {
let answer =
GetEpochsInfoResponse::decode(read(&format!("epoch-probe-pv{signed}.bin")).as_slice())
.unwrap();
for rules_version in [13, 14] {
for rules_version in [13, 14, 15] {
assert_eq!(verified_epoch_probe(answer.clone(), rules_version), signed);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ pub(crate) fn private_document_entries_from_response(
})
})
.collect::<Result<Vec<_>, Error>>()?;
// The response carries no chunk power; only a proof binds it.
Ok(PrivateDocumentEntries {
entries,
total_count,
chunk_power: None,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ the network runs a protocol version that serves it. When it does not know that y
asks for the genesis epoch first. `rs-sdk`'s protocol gate tests replay these answers
through the normal proof and signature verifier:

- `epoch-probe-pv13.bin`, `epoch-probe-pv14.bin`: the genesis epoch
- `epoch-probe-pv13.bin`, `epoch-probe-pv14.bin`, `epoch-probe-pv15.bin`: the genesis epoch
(`GetEpochsInfoRequestV0 { start_epoch: Some(0), count: 1, ascending: true, prove: true }`)
proved by a node at genesis state of protocol 13 or 14, signed at that version.
proved by a node at genesis state of protocol 13, 14 or 15, signed at that version.
- `epoch-probe-newer.bin`: the same answer from a protocol 14 state, signed as protocol
version 1000, far above any real version, so the SDK always reads it with the newest
rules it knows.
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ impl Drive {
Ok(PrivateDocumentEntries {
entries,
total_count: page.total_count,
chunk_power: Some(chunk_power),
})
}
}
Loading
Loading