Skip to content
Draft
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
25 changes: 5 additions & 20 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions packages/dash-platform-queries/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [
dash-context-provider = { path = "../rs-context-provider", default-features = false }
dash-platform-macros = { path = "../rs-dash-platform-macros" }
dpp = { path = "../rs-dpp", default-features = false, features = [
"dashpay-contract",
"dpns-contract",
"platform-value-cbor",
"state-transitions",
"state-transition-validation",
Expand Down
190 changes: 190 additions & 0 deletions packages/dash-platform-queries/src/dashpay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//! Transport-free DashPay document assembly.
//!
//! The Sdk-bound DashPay surface (ECDH, encryption, fetching the recipient,
//! broadcasting) lives in `dash-sdk`; this is the pure DIP-15 document
//! assembly it shares with embedders that hold the encrypted material
//! themselves. Field size bounds come from the DashPay contract schema, so
//! the builder cannot drift from what the contract accepts.

use crate::dpns_usernames::new_document;
use crate::Error;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
use dpp::data_contract::document_type::DocumentTypeRef;
use dpp::data_contract::errors::DataContractError;
use dpp::document::Document;
use dpp::platform_value::Value;
use dpp::prelude::{DataContract, Identifier};
use dpp::system_data_contracts::dashpay_contract::v1::document_types::contact_request;
use std::collections::BTreeMap;

/// Inputs of a DIP-15 `contactRequest` document. The encrypted fields are
/// supplied already encrypted (ECDH, AES-CBC with a fresh IV prepended);
/// see `dash-sdk`'s `create_contact_request` for the encryption itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContactRequestDocumentParams {
/// Identity sending the request; becomes the document owner.
pub sender_id: Identifier,
/// Identity receiving the request (`toUserId`).
pub recipient_id: Identifier,
/// Index of the sender's encryption key used for ECDH.
pub sender_key_index: u32,
/// Index of the recipient's key used for ECDH.
pub recipient_key_index: u32,
/// DashPay receiving-account reference.
pub account_reference: u32,
/// Encrypted DIP-15 compact extended public key (IV ‖ ciphertext).
pub encrypted_public_key: Vec<u8>,
/// Encrypted account label (IV ‖ ciphertext), if any.
pub encrypted_account_label: Option<Vec<u8>>,
/// Unencrypted auto-accept proof, if any.
pub auto_accept_proof: Option<Vec<u8>>,
/// Entropy the document id derives from; reuse it on the create
/// transition.
pub entropy: [u8; 32],
}

/// Assemble a `contactRequest` document, checking every byte-array field
/// against the size bounds the contract schema declares for it.
pub fn build_contact_request_document(
contract: &DataContract,
params: ContactRequestDocumentParams,
) -> Result<Document, Error> {
let document_type = contract.document_type_for_name(contact_request::NAME)?;

check_byte_field(
document_type,
"encryptedPublicKey",
&params.encrypted_public_key,
)?;
if let Some(label) = &params.encrypted_account_label {
check_byte_field(document_type, "encryptedAccountLabel", label)?;
}
if let Some(proof) = &params.auto_accept_proof {
check_byte_field(document_type, "autoAcceptProof", proof)?;
}

let mut properties = BTreeMap::from([
(
contact_request::properties::TO_USER_ID.to_string(),
Value::Identifier(params.recipient_id.to_buffer()),
),
(
"encryptedPublicKey".to_string(),
Value::Bytes(params.encrypted_public_key),
),
(
"senderKeyIndex".to_string(),
Value::U32(params.sender_key_index),
),
(
"recipientKeyIndex".to_string(),
Value::U32(params.recipient_key_index),
),
(
"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(new_document(
contract,
document_type.name(),
params.sender_id,
params.entropy,
properties,
))
}

/// Check `bytes` against the `minItems`/`maxItems` the contract declares for
/// the byte-array property `field`.
fn check_byte_field(
document_type: DocumentTypeRef,
field: &str,
bytes: &[u8],
) -> Result<(), Error> {
let property = document_type.properties().get(field).ok_or_else(|| {
DataContractError::DocumentTypeFieldNotFound(format!(
"{} has no property {field}",
document_type.name()
))
})?;
let min = property.property_type.min_size().unwrap_or(0) as usize;
let max = property.property_type.max_size().unwrap_or(u16::MAX) as usize;
if bytes.len() < min || bytes.len() > max {
return Err(Error::Config(format!(
"{field} must be {min}-{max} bytes, got {}",
bytes.len()
)));
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use dpp::document::DocumentV0Getters;
use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
use dpp::version::PlatformVersion;

fn params(encrypted_public_key: Vec<u8>) -> ContactRequestDocumentParams {
ContactRequestDocumentParams {
sender_id: Identifier::from([1u8; 32]),
recipient_id: Identifier::from([2u8; 32]),
sender_key_index: 1,
recipient_key_index: 2,
account_reference: 3,
encrypted_public_key,
encrypted_account_label: None,
auto_accept_proof: None,
entropy: [7u8; 32],
}
}

fn contract() -> DataContract {
load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest())
.expect("dashpay contract")
}

#[test]
fn should_derive_the_document_id_from_the_entropy() {
let contract = contract();
let document = build_contact_request_document(&contract, params(vec![0u8; 96]))
.expect("valid contact request");
assert_eq!(
document.id(),
Document::generate_document_id_v0(
&contract.id(),
&Identifier::from([1u8; 32]),
contact_request::NAME,
&[7u8; 32]
)
);
assert_eq!(document.owner_id(), Identifier::from([1u8; 32]));
assert_eq!(
document.get("toUserId"),
Some(&Value::Identifier([2u8; 32]))
);
}

#[test]
fn should_enforce_the_contract_byte_bounds() {
let contract = contract();
build_contact_request_document(&contract, params(vec![0u8; 95]))
.expect_err("encryptedPublicKey below the schema's 96 bytes");
let mut with_label = params(vec![0u8; 96]);
with_label.encrypted_account_label = Some(vec![0u8; 81]);
build_contact_request_document(&contract, with_label)
.expect_err("encryptedAccountLabel above the schema's 80 bytes");
let mut with_proof = params(vec![0u8; 96]);
with_proof.auto_accept_proof = Some(vec![0u8; 37]);
build_contact_request_document(&contract, with_proof)
.expect_err("autoAcceptProof below the schema's 38 bytes");
}
}
Loading
Loading