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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/dash-platform-queries/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mocks = [
]

[dependencies]
ciborium = { version = "0.2.2" }
dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [
"platform",
"client",
Expand Down
21 changes: 11 additions & 10 deletions packages/dash-platform-queries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@ If you want networking, retries, and a managed connection pool, use

## What's here

- `DocumentQuery` — rich document query builder with wire
encoding for both request versions.
- `DocumentQuery` — rich document query builder, wire encoding for both
request versions, and decoding **from** the wire request
(`DocumentQuery::try_from_request`) via decoders that mirror the server's
(`drive-abci`'s `v1/conversions.rs`) and are kept in lockstep with them.
- `verify_documents_response` — request-driven proof verification for document
queries, delegating to `drive-proof-verifier`'s `FromProof`.
- Aggregate proof helpers (count/sum/average/ranked) shared with `dash-sdk`.
- DPNS username helpers — label normalization/validation and the
convertibility/contested checks shared with `dash-sdk`.
- `transition::validation` — structural validation for state transitions
ahead of signing.

Wire-request decoding (`DocumentQuery::try_from_request`), request-driven
proof verification, and pure DPNS/DashPay document builders arrive in the
next slice of this series.
- Pure DPNS builders — `build_dpns_preorder_and_domain_documents`, label
normalization/validation — and pure DashPay contact-request document
assembly (`dashpay::build_contact_request_document`); crypto material is
supplied by the caller, keys never enter this crate.
- `transition::validation` helpers.

## Feature flags

Expand Down
159 changes: 159 additions & 0 deletions packages/dash-platform-queries/src/dashpay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! Transport-free DashPay contact request document assembly.
//!
//! The Sdk-bound DashPay surface (recipient fetching, ECDH, encryption,
//! broadcasting) lives in `dash-sdk`; this module is the pure DIP-15
//! `contactRequest` document assembly it shares with embedders. All crypto
//! material arrives here as bytes — key derivation and encryption stay with
//! the caller.

use crate::Error;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
use dpp::data_contract::DataContract;
use dpp::document::{Document, DocumentV0};
use dpp::platform_value::Value;
use dpp::prelude::Identifier;
use std::collections::BTreeMap;

/// Already-derived crypto material and metadata for a DIP-15
/// `contactRequest` document.
///
/// Everything here is plain data: the ECDH/encryption that produced
/// `encrypted_public_key` and `encrypted_account_label`, and the randomness
/// that produced `entropy`, happen in the caller (`dash-sdk` or an
/// embedder).
#[derive(Debug, Clone)]
pub struct ContactRequestDocumentParams {
/// The sender's identity id (the document owner)
pub sender_id: Identifier,
/// The recipient's identity id (`toUserId`)
pub recipient_id: Identifier,
/// The sender's encryption key index used for ECDH
pub sender_key_index: u32,
/// The recipient's key index used for ECDH
pub recipient_key_index: u32,
/// Reference to the DashPay receiving account
pub account_reference: u32,
/// ECDH-encrypted extended public key: exactly 96 bytes
/// (16-byte IV + 80 bytes of encrypted DIP-15 compact xpub)
pub encrypted_public_key: Vec<u8>,
/// Optional encrypted account label: 48-80 bytes
/// (16-byte IV + 32-64 bytes of encrypted data)
pub encrypted_account_label: Option<Vec<u8>>,
/// Optional auto-accept proof (38-102 bytes) - not encrypted
pub auto_accept_proof: Option<Vec<u8>>,
/// The entropy that derives the document id; the same entropy must be
/// attached to the create transition, or platform consensus rejects it
/// with `InvalidDocumentTransitionIdError`.
pub entropy: [u8; 32],
Comment on lines +18 to +48

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Document fresh IV requirements for public DashPay ciphertexts

The public transport-free builder accepts pre-encrypted DashPay fields and validates only their lengths, but its contract does not tell embedders that the first 16 bytes must contain a fresh, unpredictable AES-CBC IV generated independently for every encrypted field and contact request. The managed SDK follows this rule by filling separate IVs from StdRng::from_entropy(). Reusing an IV with the same ECDH-derived key exposes equality of plaintext prefixes and allows public on-chain ciphertexts to be correlated. Document the fresh-IV requirement, recommend the canonical platform_encryption format helpers, and state that entropy must also be freshly generated for each document to avoid document-ID reuse.

source: ['codex']

}

/// Validate the size of a DIP-15 `autoAcceptProof` (38-102 bytes).
pub fn validate_auto_accept_proof(proof: &[u8]) -> Result<(), Error> {
if proof.len() < 38 || proof.len() > 102 {
return Err(Error::InvalidInput(format!(
"autoAcceptProof must be 38-102 bytes, got {}",
proof.len()
)));
}
Ok(())
}

/// Build a DIP-15 `contactRequest` document from already-derived crypto
/// material.
///
/// This is the pure document-assembly half of `dash-sdk`'s
/// `create_contact_request`: the document id derives from `params.entropy`,
/// the owner is `params.sender_id`, and the properties are exactly the fields
/// the DashPay contract defines. Broadcast the returned document with the
/// same `params.entropy` attached to the create transition, or platform
/// consensus rejects it with `InvalidDocumentTransitionIdError`.
///
/// Returns the assembled `contactRequest` [`Document`].
pub fn build_contact_request_document(
contract: &DataContract,
params: ContactRequestDocumentParams,
) -> Result<Document, Error> {
if let Some(ref proof) = params.auto_accept_proof {
validate_auto_accept_proof(proof)?;
}

// Validate encrypted public key size (must be exactly 96 bytes: 16-byte IV + 80-byte encrypted data)
if params.encrypted_public_key.len() != 96 {
return Err(Error::InvalidInput(format!(
"Encrypted public key size mismatch: expected 96 bytes, got {}",
params.encrypted_public_key.len()
)));
}

// Validate encrypted label size (48-80 bytes: 16-byte IV + 32-64 byte encrypted data)
if let Some(ref label) = params.encrypted_account_label {
if label.len() < 48 || label.len() > 80 {
return Err(Error::InvalidInput(format!(
"Encrypted account label size out of range: expected 48-80 bytes, got {}",
label.len()
)));
}
}

let contact_request_document_type =
contract
.document_type_for_name("contactRequest")
.map_err(|_| {
Error::InvalidInput("DashPay contactRequest document type not found".to_string())
})?;

let document_id = Document::generate_document_id_v0(
&contract.id(),
&params.sender_id,
contact_request_document_type.name(),
params.entropy.as_slice(),
);

let mut properties = BTreeMap::new();
properties.insert(
"toUserId".to_string(),
Value::Identifier(params.recipient_id.to_buffer()),
);
properties.insert(
"encryptedPublicKey".to_string(),
Value::Bytes(params.encrypted_public_key),
);
properties.insert(
"senderKeyIndex".to_string(),
Value::U32(params.sender_key_index),
);
properties.insert(
"recipientKeyIndex".to_string(),
Value::U32(params.recipient_key_index),
);
properties.insert(
"accountReference".to_string(),
Value::U32(params.account_reference),
);

if let Some(label) = params.encrypted_account_label {
properties.insert("encryptedAccountLabel".to_string(), Value::Bytes(label));
}
if let Some(proof) = params.auto_accept_proof {
properties.insert("autoAcceptProof".to_string(), Value::Bytes(proof));
}

Ok(Document::V0(DocumentV0 {
contract_version: None,
id: document_id,
owner_id: params.sender_id,
properties,
revision: None,
created_at: None,
updated_at: None,
transferred_at: None,
created_at_block_height: None,
updated_at_block_height: None,
transferred_at_block_height: None,
created_at_core_block_height: None,
updated_at_core_block_height: None,
transferred_at_core_block_height: None,
creator_id: None,
}))
}
Loading
Loading