Skip to content

Commit 47b05e3

Browse files
feat(sdk): pure DPNS and DashPay document builders shared with embedders
dash-platform-queries gains build_dpns_preorder_document / build_dpns_domain_document / salted_domain_hash (dpns_usernames) and build_contact_request_document (new dashpay module): the document-assembly halves of dash-sdk's register_dpns_name and create_contact_request as pure functions that take caller-supplied entropy, salt and ciphertexts and touch no network or randomness. dash-sdk's networked flows now call them, so an embedder that assembles its own transitions (the Dash Core platform GUI) and the SDK share one implementation. The builders lean on what the codebase already has rather than re-deriving it: normalization is dpp's consensus convert_to_homograph_safe_chars (the crate's ASCII-only copy, whose non-ASCII behaviour differed from the data trigger's, is replaced by a re-export); the preorder commitment uses dpp::util::hash::hash_double; property names come from the dpns-contract / dashpay-contract constants; and the DashPay byte-array bounds (96 / 48-80 / 38-102) are read from the contract schema instead of being hard-coded. The DPNS domain builder rejects a label the contract's pattern refuses before a preorder is paid for. The entropy/document-id consistency check moves into dpp's DocumentCreateTransitionV0::from_document, where every create-transition caller (SDK, wasm, FFI, embedders) inherits it instead of having to remember an opt-in helper; it refuses with the same InvalidDocumentTransitionIdError Drive would return after the nonce bump. dash-sdk's private ensure_entropy_matches_document_id and its tests are removed in favour of that. API shape (unreleased v4.2-dev): ContactRequestResult now carries the assembled document plus entropy instead of id/owner_id/properties; send_contact_request no longer hand-rebuilds a DocumentV0.
1 parent b84975e commit 47b05e3

11 files changed

Lines changed: 568 additions & 357 deletions

File tree

‎packages/dash-platform-queries/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [
2424
dash-context-provider = { path = "../rs-context-provider", default-features = false }
2525
dash-platform-macros = { path = "../rs-dash-platform-macros" }
2626
dpp = { path = "../rs-dpp", default-features = false, features = [
27+
"dashpay-contract",
28+
"dpns-contract",
2729
"platform-value-cbor",
2830
"state-transitions",
2931
"state-transition-validation",
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
//! Transport-free DashPay document assembly.
2+
//!
3+
//! The Sdk-bound DashPay surface (ECDH, encryption, fetching the recipient,
4+
//! broadcasting) lives in `dash-sdk`; this is the pure DIP-15 document
5+
//! assembly it shares with embedders that hold the encrypted material
6+
//! themselves. Field size bounds come from the DashPay contract schema, so
7+
//! the builder cannot drift from what the contract accepts.
8+
9+
use crate::dpns_usernames::new_document;
10+
use crate::Error;
11+
use dpp::data_contract::accessors::v0::DataContractV0Getters;
12+
use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
13+
use dpp::data_contract::document_type::DocumentTypeRef;
14+
use dpp::data_contract::errors::DataContractError;
15+
use dpp::document::Document;
16+
use dpp::platform_value::Value;
17+
use dpp::prelude::{DataContract, Identifier};
18+
use dpp::system_data_contracts::dashpay_contract::v1::document_types::contact_request;
19+
use std::collections::BTreeMap;
20+
21+
/// Inputs of a DIP-15 `contactRequest` document. The encrypted fields are
22+
/// supplied already encrypted (ECDH, AES-CBC with a fresh IV prepended);
23+
/// see `dash-sdk`'s `create_contact_request` for the encryption itself.
24+
#[derive(Debug, Clone, PartialEq, Eq)]
25+
pub struct ContactRequestDocumentParams {
26+
/// Identity sending the request; becomes the document owner.
27+
pub sender_id: Identifier,
28+
/// Identity receiving the request (`toUserId`).
29+
pub recipient_id: Identifier,
30+
/// Index of the sender's encryption key used for ECDH.
31+
pub sender_key_index: u32,
32+
/// Index of the recipient's key used for ECDH.
33+
pub recipient_key_index: u32,
34+
/// DashPay receiving-account reference.
35+
pub account_reference: u32,
36+
/// Encrypted DIP-15 compact extended public key (IV ‖ ciphertext).
37+
pub encrypted_public_key: Vec<u8>,
38+
/// Encrypted account label (IV ‖ ciphertext), if any.
39+
pub encrypted_account_label: Option<Vec<u8>>,
40+
/// Unencrypted auto-accept proof, if any.
41+
pub auto_accept_proof: Option<Vec<u8>>,
42+
/// Entropy the document id derives from; reuse it on the create
43+
/// transition.
44+
pub entropy: [u8; 32],
45+
}
46+
47+
/// Assemble a `contactRequest` document, checking every byte-array field
48+
/// against the size bounds the contract schema declares for it.
49+
pub fn build_contact_request_document(
50+
contract: &DataContract,
51+
params: ContactRequestDocumentParams,
52+
) -> Result<Document, Error> {
53+
let document_type = contract.document_type_for_name(contact_request::NAME)?;
54+
55+
check_byte_field(
56+
document_type,
57+
"encryptedPublicKey",
58+
&params.encrypted_public_key,
59+
)?;
60+
if let Some(label) = &params.encrypted_account_label {
61+
check_byte_field(document_type, "encryptedAccountLabel", label)?;
62+
}
63+
if let Some(proof) = &params.auto_accept_proof {
64+
check_byte_field(document_type, "autoAcceptProof", proof)?;
65+
}
66+
67+
let mut properties = BTreeMap::from([
68+
(
69+
contact_request::properties::TO_USER_ID.to_string(),
70+
Value::Identifier(params.recipient_id.to_buffer()),
71+
),
72+
(
73+
"encryptedPublicKey".to_string(),
74+
Value::Bytes(params.encrypted_public_key),
75+
),
76+
(
77+
"senderKeyIndex".to_string(),
78+
Value::U32(params.sender_key_index),
79+
),
80+
(
81+
"recipientKeyIndex".to_string(),
82+
Value::U32(params.recipient_key_index),
83+
),
84+
(
85+
"accountReference".to_string(),
86+
Value::U32(params.account_reference),
87+
),
88+
]);
89+
if let Some(label) = params.encrypted_account_label {
90+
properties.insert("encryptedAccountLabel".to_string(), Value::Bytes(label));
91+
}
92+
if let Some(proof) = params.auto_accept_proof {
93+
properties.insert("autoAcceptProof".to_string(), Value::Bytes(proof));
94+
}
95+
96+
Ok(new_document(
97+
contract,
98+
document_type.name(),
99+
params.sender_id,
100+
params.entropy,
101+
properties,
102+
))
103+
}
104+
105+
/// Check `bytes` against the `minItems`/`maxItems` the contract declares for
106+
/// the byte-array property `field`.
107+
fn check_byte_field(
108+
document_type: DocumentTypeRef,
109+
field: &str,
110+
bytes: &[u8],
111+
) -> Result<(), Error> {
112+
let property = document_type.properties().get(field).ok_or_else(|| {
113+
DataContractError::DocumentTypeFieldNotFound(format!(
114+
"{} has no property {field}",
115+
document_type.name()
116+
))
117+
})?;
118+
let min = property.property_type.min_size().unwrap_or(0) as usize;
119+
let max = property.property_type.max_size().unwrap_or(u16::MAX) as usize;
120+
if bytes.len() < min || bytes.len() > max {
121+
return Err(Error::Config(format!(
122+
"{field} must be {min}-{max} bytes, got {}",
123+
bytes.len()
124+
)));
125+
}
126+
Ok(())
127+
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::*;
132+
use dpp::document::DocumentV0Getters;
133+
use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
134+
use dpp::version::PlatformVersion;
135+
136+
fn params(encrypted_public_key: Vec<u8>) -> ContactRequestDocumentParams {
137+
ContactRequestDocumentParams {
138+
sender_id: Identifier::from([1u8; 32]),
139+
recipient_id: Identifier::from([2u8; 32]),
140+
sender_key_index: 1,
141+
recipient_key_index: 2,
142+
account_reference: 3,
143+
encrypted_public_key,
144+
encrypted_account_label: None,
145+
auto_accept_proof: None,
146+
entropy: [7u8; 32],
147+
}
148+
}
149+
150+
fn contract() -> DataContract {
151+
load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest())
152+
.expect("dashpay contract")
153+
}
154+
155+
#[test]
156+
fn should_derive_the_document_id_from_the_entropy() {
157+
let contract = contract();
158+
let document = build_contact_request_document(&contract, params(vec![0u8; 96]))
159+
.expect("valid contact request");
160+
assert_eq!(
161+
document.id(),
162+
Document::generate_document_id_v0(
163+
&contract.id(),
164+
&Identifier::from([1u8; 32]),
165+
contact_request::NAME,
166+
&[7u8; 32]
167+
)
168+
);
169+
assert_eq!(document.owner_id(), Identifier::from([1u8; 32]));
170+
assert_eq!(
171+
document.get("toUserId"),
172+
Some(&Value::Identifier([2u8; 32]))
173+
);
174+
}
175+
176+
#[test]
177+
fn should_enforce_the_contract_byte_bounds() {
178+
let contract = contract();
179+
build_contact_request_document(&contract, params(vec![0u8; 95]))
180+
.expect_err("encryptedPublicKey below the schema's 96 bytes");
181+
let mut with_label = params(vec![0u8; 96]);
182+
with_label.encrypted_account_label = Some(vec![0u8; 81]);
183+
build_contact_request_document(&contract, with_label)
184+
.expect_err("encryptedAccountLabel above the schema's 80 bytes");
185+
let mut with_proof = params(vec![0u8; 96]);
186+
with_proof.auto_accept_proof = Some(vec![0u8; 37]);
187+
build_contact_request_document(&contract, with_proof)
188+
.expect_err("autoAcceptProof below the schema's 38 bytes");
189+
}
190+
}

0 commit comments

Comments
 (0)