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
16 changes: 16 additions & 0 deletions book/src/evo-sdk/dashpay-contact-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ The current DashPay contract schema requires the system field
`$createdAtCoreBlockHeight`. Older external references may use
`coreHeightCreatedAt`; do not submit that name to the current contract.

### What consensus checks

From protocol version 14 the contract declares these checks itself, and a
contact request that fails one is refused (and the fee charged):

| Rule | Declared as | Error |
| --- | --- | --- |
| `toUserId` is not the sender | `distinctFrom: "$ownerId"` on `toUserId` | `DocumentPropertyNotDistinctError` (10419) |
| The recipient identity exists and has the key `recipientKeyIndex` | `refersTo` of type `identityPublicKey` on `toUserId`, `keyIdProperty: "recipientKeyIndex"` | `ReferencedIdentityKeyNotFoundError` (40123) |
| That key is not disabled | the same `refersTo` | `ReferencedIdentityKeyDisabledError` (40124) |
| `encryptedPublicKey` and `encryptedAccountLabel` are a 16-byte IV plus whole 16-byte blocks | `encryptedFor` with scheme `ecdh-secp256k1-aes256-cbc` | `InvalidEncryptedPropertyShapeError` (10420) |

Up to protocol version 13 a data trigger made only the first two checks and
reported both as a `DataTriggerConditionError`. Neither version checks the
purpose or contract bounds of either key, or that the bytes decrypt.

`encryptedPublicKey` is exactly 96 bytes:

- 16 bytes: AES-CBC initialization vector
Expand Down
24 changes: 20 additions & 4 deletions packages/dashpay-contract/schema/v2/dashpay.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,20 @@
"byteArray": true,
"minItems": 21,
"maxItems": 21,
"description": "Core chain address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by the 20-byte HASH160, i.e. RIPEMD160 of SHA256, of the public key or redeem script) for public payments. The type byte is consensus-enforced by a data trigger; clients render the address as Base58Check for the network they are on. Payments to it are publicly linkable to this profile.",
"position": 5
},
"platformPaymentAddress": {
"type": "array",
"byteArray": true,
"minItems": 21,
"maxItems": 21,
"description": "Platform address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by the 20-byte HASH160, i.e. RIPEMD160 of SHA256, of the public key or redeem script) for public payments. The type byte is consensus-enforced by a data trigger.",
"position": 6
},
"shieldedAddress": {
"type": "array",
"byteArray": true,
"minItems": 43,
"maxItems": 43,
"description": "Raw Orchard receiving address: 11-byte diversifier followed by 32-byte diversified transmission key. Clients validate before payment; wallets should use a dedicated tip account.",
"position": 7
}
},
Expand Down Expand Up @@ -226,23 +223,36 @@
"minItems": 32,
"maxItems": 32,
"position": 0,
"contentMediaType": "application/x.dash.dpp.identifier"
"contentMediaType": "application/x.dash.dpp.identifier",
"refersTo": {
"type": "identityPublicKey",
"keyIdProperty": "recipientKeyIndex"
},
"distinctFrom": "$ownerId"
},
"encryptedPublicKey": {
"type": "array",
"byteArray": true,
"minItems": 96,
"maxItems": 96,
"encryptedFor": {
"recipient": "toUserId",
"recipientKey": "recipientKeyIndex",
"senderKey": "senderKeyIndex",
"scheme": "ecdh-secp256k1-aes256-cbc"
},
"position": 1
},
"senderKeyIndex": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"position": 2
},
"recipientKeyIndex": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"position": 3
},
"accountReference": {
Expand All @@ -255,6 +265,12 @@
"byteArray": true,
"minItems": 48,
"maxItems": 80,
"encryptedFor": {
"recipient": "toUserId",
"recipientKey": "recipientKeyIndex",
"senderKey": "senderKeyIndex",
"scheme": "ecdh-secp256k1-aes256-cbc"
},
"position": 5
},
"autoAcceptProof": {
Expand Down
22 changes: 20 additions & 2 deletions packages/dashpay-contract/src/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@ use crate::error::Error;
use serde_json::Value;

// Document-type name and property constants live in `crate::v1::document_types`;
// v2 does not change any names v1 defined, it only adds the optional
// v2 does not change any names v1 defined. It adds the optional
// `corePaymentAddress`, `platformPaymentAddress`, and `shieldedAddress`
// properties to `profile`.
// properties to `profile`, and declares on `contactRequest` what consensus
// checks (`toUserId`: `distinctFrom` and an `identityPublicKey` `refersTo`;
// `encryptedPublicKey` / `encryptedAccountLabel`: `encryptedFor`).
//
// v2 replaces v1 in place at protocol version 14 without the contract update
// checks, and the contract keeps `sizedIntegerTypes` off, so every property v1
// declares must keep its stored encoding: a key reference on the key id
// property itself (`identityProperty`) would store it as a u32 instead of an
// i64 and misread every stored document.
//
// The schema carries no descriptions for the three profile addresses, to keep
// the stored contract small. Their formats:
// - `corePaymentAddress` / `platformPaymentAddress`: 21 bytes, a type byte
// (0x00 P2PKH, 0x01 P2SH, enforced by the profile data trigger) followed by
// the 20-byte HASH160. Clients render the Core one as Base58Check for their
// network. Payments to either are publicly linkable to the profile.
// - `shieldedAddress`: 43 bytes, a raw Orchard address (11-byte diversifier
// then the 32-byte diversified transmission key). Only the length is checked
// on chain; clients validate it before paying.

pub fn load_documents_schemas() -> Result<Value, Error> {
serde_json::from_str(include_str!("../../schema/v2/dashpay.schema.json"))
Expand Down
91 changes: 91 additions & 0 deletions packages/rs-dpp/src/system_data_contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,94 @@ mod app_connect_tests {
);
}
}

#[cfg(all(test, feature = "dashpay-contract"))]
mod dashpay_tests {
use super::*;
use crate::data_contract::accessors::v0::DataContractV0Getters;
use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
use crate::data_contract::document_type::{
DistinctFrom, DocumentPropertyReferenceTarget, DocumentPropertyType, EncryptedFor,
EncryptedForRecipient, EncryptionScheme, IdentityKeyReferenceRequirements,
};

/// DashPay v2 replaces v1 in place at protocol version 14 (`apply_contract` in
/// `transition_to_version_14`), which never runs the contract update checks, so
/// documents written under v1 are read with v2's types from then on. Every
/// property v1 declares must be stored exactly as v1 stored it.
#[test]
fn should_store_every_dashpay_v1_property_as_v1_did() {
let v1 = load_system_data_contract(
SystemDataContract::Dashpay,
PlatformVersion::get(13).expect("protocol version 13"),
)
.expect("dashpay v1");
let v2 = load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest())
.expect("dashpay v2");

for (type_name, v1_type) in v1.document_types() {
let v2_type = v2
.document_type_for_name(type_name)
.expect("v2 keeps every v1 document type");
for (path, v1_property) in v1_type.flattened_properties() {
let v2_property = v2_type
.flattened_properties()
.get(path)
.unwrap_or_else(|| panic!("v2 keeps {type_name}.{path}"));
assert_eq!(
v1_property.property_type.stored_encoding(),
v2_property.property_type.stored_encoding(),
"{type_name}.{path} must be stored as v1 stored it"
);
}
}
}

/// From protocol version 14 the contact request's recipient checks are schema
/// declarations instead of a data trigger, and the two ECDH fields name their
/// encryption recipe.
#[test]
fn should_declare_the_contact_request_checks_and_encryption() {
let contract =
load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest())
.expect("dashpay v2");
let contact_request = contract
.document_type_for_name("contactRequest")
.expect("contactRequest");
let to_user_id = contact_request
.flattened_properties()
.get("toUserId")
.expect("toUserId");

assert_eq!(
to_user_id.property_type,
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::IdentityPublicKey {
key_id_property: "recipientKeyIndex".to_string(),
key_requirements: IdentityKeyReferenceRequirements::default(),
}
)
);
assert_eq!(to_user_id.distinct_from, Some(DistinctFrom::OwnerId));

let recipe = EncryptedFor {
recipient: EncryptedForRecipient::Property("toUserId".to_string()),
recipient_key: "recipientKeyIndex".to_string(),
sender_key: "senderKeyIndex".to_string(),
scheme: EncryptionScheme::EcdhSecp256k1Aes256Cbc,
};
let mut encrypted = contact_request
.encrypted_properties()
.into_iter()
.map(|(path, encrypted_for)| (path.as_str(), encrypted_for.clone()))
.collect::<Vec<_>>();
encrypted.sort_by_key(|(path, _)| *path);
assert_eq!(
encrypted,
vec![
("encryptedAccountLabel", recipe.clone()),
("encryptedPublicKey", recipe),
]
);
}
}
14 changes: 7 additions & 7 deletions packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ mod tests {
// from protocol version 14 the contract's version item is stored in the contract's
// other tree (one more tree insert), the config is version 2 (one more byte), and
// the larger DashPay v2 schema adds byte-billed contract bytes
24003037140
24002860070
);

let check_result = platform
Expand Down Expand Up @@ -1045,7 +1045,7 @@ mod tests {
// We have one invalid paid for state transition
assert_eq!(processing_result.invalid_paid_count(), 1);

assert_eq!(processing_result.aggregated_fees().processing_fee, 346660);
assert_eq!(processing_result.aggregated_fees().processing_fee, 346820);

let check_result = platform
.check_tx(
Expand Down Expand Up @@ -1358,7 +1358,7 @@ mod tests {
// from protocol version 14 the contract's version item is stored in the contract's
// other tree (one more tree insert), the config is version 2 (one more byte), and
// the larger DashPay v2 schema adds byte-billed contract bytes
24006074280
24005720140
);

let check_result = platform
Expand Down Expand Up @@ -1836,7 +1836,7 @@ mod tests {
// from protocol version 14 the contract's version item is stored in the contract's
// other tree (one more tree insert), the config is version 2 (one more byte), and
// the larger DashPay v2 schema adds byte-billed contract bytes
24003037140
24002860070
);

platform
Expand Down Expand Up @@ -1927,7 +1927,7 @@ mod tests {
// other tree (an update reads what key `2` holds, billed, before writing under it),
// the config is version 2, and the larger DashPay v2 schema adds byte-billed
// contract bytes
27003119120
27002916290
);

let check_result = platform
Expand Down Expand Up @@ -2518,7 +2518,7 @@ mod tests {
// from protocol version 14 the contract's version item is stored in the contract's
// other tree (one more tree insert), the config is version 2 (one more byte), and
// the larger DashPay v2 schema adds byte-billed contract bytes
24003037140
24002860070
);

platform
Expand Down Expand Up @@ -2641,7 +2641,7 @@ mod tests {
// We have one invalid paid for state transition
assert_eq!(processing_result.invalid_paid_count(), 1);

assert_eq!(processing_result.aggregated_fees().processing_fee, 448640);
assert_eq!(processing_result.aggregated_fees().processing_fee, 448800);

let check_result = platform
.check_tx(
Expand Down
Loading
Loading