Skip to content

Commit edb833b

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. This is a move, not a rewrite: the documents these builders produce are byte for byte what the inline SDK code produced. 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 two agree on every label the contract's ASCII-only pattern admits); 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 to the same numbers. Deliberately not changed here, to keep this reviewable as a pure extraction: - No new label validation. An earlier draft rejected labels via is_valid_username before the preorder was paid for, but that helper is stricter than the DPNS contract (it also refuses consecutive hyphens, which the contract's pattern admits), so it would have refused names Platform accepts. Failing early on a bad label is worth doing on its own, against the contract's actual pattern; it is not this PR. - No entropy/document-id consistency check in dpp. dash-sdk keeps its existing private ensure_entropy_matches_document_id. Hoisting that into DocumentCreateTransitionV0::from_document so every caller inherits it is a good change, but it adds an error path to a shared crate and is separable. The autoAcceptProof bound is still checked in create_contact_request before the recipient lookup: the shared builder re-checks it against the schema, but that field is raw caller input and the lookup is a network round trip. The two checks on the SDK's own encryption output are dropped as dead code — the pre-existing COMPACT_XPUB_LEN guard forces the encrypted xpub to 96 bytes and fit_account_label bounds the encrypted label to 48-80 — and the builder covers both for embedders that do their own encryption. 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 63cf57f commit edb833b

8 files changed

Lines changed: 534 additions & 252 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: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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+
// Only `Array`/`VariableTypeArray` report no size, and dpp refuses those
119+
// at contract creation ("only byte arrays are supported now"), so every
120+
// property reachable here is sized and the defaults never apply.
121+
let min = property.property_type.min_size().unwrap_or(0) as usize;
122+
let max = property.property_type.max_size().unwrap_or(u16::MAX) as usize;
123+
if bytes.len() < min || bytes.len() > max {
124+
return Err(Error::Config(format!(
125+
"{field} must be {min}-{max} bytes, got {}",
126+
bytes.len()
127+
)));
128+
}
129+
Ok(())
130+
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
135+
use dpp::document::DocumentV0Getters;
136+
use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
137+
use dpp::version::PlatformVersion;
138+
139+
fn params(encrypted_public_key: Vec<u8>) -> ContactRequestDocumentParams {
140+
ContactRequestDocumentParams {
141+
sender_id: Identifier::from([1u8; 32]),
142+
recipient_id: Identifier::from([2u8; 32]),
143+
sender_key_index: 1,
144+
recipient_key_index: 2,
145+
account_reference: 3,
146+
encrypted_public_key,
147+
encrypted_account_label: None,
148+
auto_accept_proof: None,
149+
entropy: [7u8; 32],
150+
}
151+
}
152+
153+
fn contract() -> DataContract {
154+
load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest())
155+
.expect("dashpay contract")
156+
}
157+
158+
#[test]
159+
fn should_derive_the_document_id_from_the_entropy() {
160+
let contract = contract();
161+
let document = build_contact_request_document(&contract, params(vec![0u8; 96]))
162+
.expect("valid contact request");
163+
assert_eq!(
164+
document.id(),
165+
Document::generate_document_id_v0(
166+
&contract.id(),
167+
&Identifier::from([1u8; 32]),
168+
contact_request::NAME,
169+
&[7u8; 32]
170+
)
171+
);
172+
assert_eq!(document.owner_id(), Identifier::from([1u8; 32]));
173+
assert_eq!(
174+
document.get("toUserId"),
175+
Some(&Value::Identifier([2u8; 32]))
176+
);
177+
}
178+
179+
#[test]
180+
fn should_enforce_the_contract_byte_bounds() {
181+
let contract = contract();
182+
build_contact_request_document(&contract, params(vec![0u8; 95]))
183+
.expect_err("encryptedPublicKey below the schema's 96 bytes");
184+
let mut with_label = params(vec![0u8; 96]);
185+
with_label.encrypted_account_label = Some(vec![0u8; 81]);
186+
build_contact_request_document(&contract, with_label)
187+
.expect_err("encryptedAccountLabel above the schema's 80 bytes");
188+
let mut with_proof = params(vec![0u8; 96]);
189+
with_proof.auto_accept_proof = Some(vec![0u8; 37]);
190+
build_contact_request_document(&contract, with_proof)
191+
.expect_err("autoAcceptProof below the schema's 38 bytes");
192+
}
193+
194+
/// The optional fields are the ones a wire-format slip would silently
195+
/// drop, so pin every property the document carries when both are set.
196+
#[test]
197+
fn should_carry_every_property_when_the_optional_fields_are_present() {
198+
let contract = contract();
199+
let mut with_optionals = params(vec![1u8; 96]);
200+
with_optionals.encrypted_account_label = Some(vec![2u8; 64]);
201+
with_optionals.auto_accept_proof = Some(vec![3u8; 40]);
202+
203+
let document = build_contact_request_document(&contract, with_optionals)
204+
.expect("valid contact request");
205+
206+
assert_eq!(
207+
document.get("toUserId"),
208+
Some(&Value::Identifier([2u8; 32]))
209+
);
210+
assert_eq!(
211+
document.get("encryptedPublicKey"),
212+
Some(&Value::Bytes(vec![1u8; 96]))
213+
);
214+
assert_eq!(document.get("senderKeyIndex"), Some(&Value::U32(1)));
215+
assert_eq!(document.get("recipientKeyIndex"), Some(&Value::U32(2)));
216+
assert_eq!(document.get("accountReference"), Some(&Value::U32(3)));
217+
assert_eq!(
218+
document.get("encryptedAccountLabel"),
219+
Some(&Value::Bytes(vec![2u8; 64]))
220+
);
221+
assert_eq!(
222+
document.get("autoAcceptProof"),
223+
Some(&Value::Bytes(vec![3u8; 40]))
224+
);
225+
}
226+
227+
/// A contract that declares no `contactRequest` surfaces dpp's
228+
/// `DataContractError` through our `Error`, rather than panicking.
229+
#[test]
230+
fn should_refuse_a_contract_without_a_contact_request_type() {
231+
let dpns = load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest())
232+
.expect("dpns contract");
233+
let error = build_contact_request_document(&dpns, params(vec![0u8; 96]))
234+
.expect_err("dpns declares no contactRequest document type");
235+
assert!(
236+
matches!(error, Error::Protocol(_)),
237+
"unexpected error: {error:?}"
238+
);
239+
}
240+
241+
#[test]
242+
fn should_refuse_a_field_the_document_type_does_not_declare() {
243+
let contract = contract();
244+
let document_type = contract
245+
.document_type_for_name(contact_request::NAME)
246+
.expect("contactRequest document type");
247+
let error = check_byte_field(document_type, "notAProperty", &[])
248+
.expect_err("contactRequest declares no such property");
249+
assert!(
250+
matches!(error, Error::Protocol(_)),
251+
"unexpected error: {error:?}"
252+
);
253+
}
254+
}

0 commit comments

Comments
 (0)