From b3c867decd6c682d9ac6f96e8efd3b17591c598e Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 22 Aug 2026 12:19:02 -0500 Subject: [PATCH 1/8] feat(sdk): add client-side v1 document-query wire decoders Copies the wire-proto -> drive-type decoders for the v1 getDocuments surface from rs-drive-abci's query/document_query/v1/conversions.rs into dash-platform-queries::documents::proto_conversions, verbatim except for a neutral DecodeError replacing the server's QueryError with the exact same message strings. The server is untouched. This is a client-side mirror kept in lockstep by doc contract, the same convention the v0 path uses where CBOR clause decoding mirrors query_documents_v0. Hosting a single shared decode crate that both sides consume is proposed separately; this PR deliberately avoids adding any drive-abci dependency. Upcoming client-side wire decoding (DocumentQuery::try_from_request) consumes these functions; until that commit lands the module carries a temporary allow(dead_code). --- .../src/documents/mod.rs | 5 + .../src/documents/proto_conversions.rs | 377 ++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 packages/dash-platform-queries/src/documents/proto_conversions.rs diff --git a/packages/dash-platform-queries/src/documents/mod.rs b/packages/dash-platform-queries/src/documents/mod.rs index 65cde6af086..6c3d07d6b15 100644 --- a/packages/dash-platform-queries/src/documents/mod.rs +++ b/packages/dash-platform-queries/src/documents/mod.rs @@ -11,5 +11,10 @@ pub mod document_split_counts; pub mod document_split_sums; pub mod document_sum; pub(crate) mod having_proof_helpers; +/// Client-side wire-proto → drive-type decoders for `getDocuments`, +/// mirroring rs-drive-abci's server request decode; the two must be +/// kept in lockstep (see the module docs). +#[allow(dead_code)] // consumer (`DocumentQuery::try_from_request`) lands next +pub(crate) mod proto_conversions; pub(crate) mod ranked_proof_helpers; pub(crate) mod sum_proof_helpers; diff --git a/packages/dash-platform-queries/src/documents/proto_conversions.rs b/packages/dash-platform-queries/src/documents/proto_conversions.rs new file mode 100644 index 00000000000..2328310f261 --- /dev/null +++ b/packages/dash-platform-queries/src/documents/proto_conversions.rs @@ -0,0 +1,377 @@ +//! Wire-protobuf → drive type conversions for the `getDocuments` +//! query surface, used by +//! [`DocumentQuery::try_from_request`](super::document_query::DocumentQuery::try_from_request) +//! to rebuild the rich query from the wire request so a proved +//! response can be verified against exactly what was asked. +//! +//! This is a client-side **mirror** of the server's decode — +//! rs-drive-abci's `query/document_query/v1/conversions.rs` — and +//! must be kept in lockstep with it, exactly as the v0 path's CBOR +//! clause decoding mirrors `query_documents_v0`. The bytes the +//! server decodes and the bytes the verifier decodes must agree +//! clause-for-clause, or a proof could verify against a different +//! query than the server answered. Function bodies and error +//! message strings are copied verbatim from the server; any change +//! on either side must be replayed on the other. +//! +//! Conversion contract: +//! - Every fallible case maps to [`DecodeError::InvalidArgument`] +//! (malformed wire input, **not** future capability), except the +//! aggregate `ORDER BY` target which maps to +//! [`DecodeError::Unsupported`] (valid request shape, server +//! capability not yet wired). These mirror the server's +//! `QueryError::InvalidArgument` / `QuerySyntaxError::Unsupported` +//! respectively, preserving its error surface. +//! - Conversion is schema-agnostic. `DocumentFieldValue` variants +//! map 1:1 to `dpp::platform_value::Value` variants without +//! consulting the document type's schema. The schema-driven +//! coercion (`document_type.serialize_value_for_key`) runs +//! downstream as it does for the CBOR-shaped v0 path — a `text` +//! variant against an identifier field decodes via base58, a +//! `bytes_value` against the same field decodes as raw 32-byte +//! identifier, and so on. The wire layer just names the +//! primitive; the schema decides the indexed type. + +use dapi_grpc::platform::v0::get_documents_request::{ + document_field_value, + get_documents_request_v1::{select, Select as ProtoSelect}, + having_aggregate, having_clause, order_clause, DocumentFieldValue as ProtoDocumentFieldValue, + HavingAggregate as ProtoHavingAggregate, HavingClause as ProtoHavingClause, + OrderClause as ProtoOrderClause, WhereClause as ProtoWhereClause, + WhereOperator as ProtoWhereOperator, +}; +use dpp::platform_value::Value; +use drive::query::{ + HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, + OrderClause, SelectFunction, SelectProjection, WhereClause, WhereOperator, +}; + +/// Neutral decode error for the proto → drive conversions. +/// +/// Mirrors the two error shapes the server's decode produces +/// (`QueryError::InvalidArgument` and `QuerySyntaxError::Unsupported`) +/// without depending on them; the client-side `DocumentQuery` +/// decoding maps it onto the crate [`Error`](crate::error::Error). +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum DecodeError { + /// Malformed wire input — bad discriminant, missing oneof arm, + /// over-deep list nesting. No future protocol version would make + /// this input valid. + #[error("{0}")] + InvalidArgument(String), + /// Well-formed wire input naming a capability the decode target + /// cannot represent yet (e.g. `ORDER BY` on an aggregate key). + /// The wording signals future capability, not malformed request. + #[error("{0}")] + Unsupported(String), +} + +/// Map a wire-level [`ProtoWhereOperator`] discriminant onto +/// drive's [`WhereOperator`]. Unknown discriminants are wire-level +/// garbage (no future protocol value would map a malformed integer +/// to a valid behavior), so they surface as +/// [`DecodeError::InvalidArgument`]. +pub(crate) fn where_operator_from_proto(op: i32) -> Result { + let proto_op = ProtoWhereOperator::try_from(op).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown WhereOperator discriminant: {} (valid values: 0..=10, see \ + `get_documents_request::WhereOperator`)", + op + )) + })?; + Ok(match proto_op { + ProtoWhereOperator::Equal => WhereOperator::Equal, + ProtoWhereOperator::GreaterThan => WhereOperator::GreaterThan, + ProtoWhereOperator::GreaterThanOrEquals => WhereOperator::GreaterThanOrEquals, + ProtoWhereOperator::LessThan => WhereOperator::LessThan, + ProtoWhereOperator::LessThanOrEquals => WhereOperator::LessThanOrEquals, + ProtoWhereOperator::Between => WhereOperator::Between, + ProtoWhereOperator::BetweenExcludeBounds => WhereOperator::BetweenExcludeBounds, + ProtoWhereOperator::BetweenExcludeLeft => WhereOperator::BetweenExcludeLeft, + ProtoWhereOperator::BetweenExcludeRight => WhereOperator::BetweenExcludeRight, + ProtoWhereOperator::In => WhereOperator::In, + ProtoWhereOperator::StartsWith => WhereOperator::StartsWith, + }) +} + +/// Map a wire [`ProtoDocumentFieldValue`] onto a +/// `dpp::platform_value::Value`. Schema-agnostic — variants map +/// 1:1 by primitive type and recurse for `list` up to a depth of +/// 1 (the only nesting level the query surface needs: `IN` / +/// `BETWEEN*` take a flat list of scalars). Anything deeper is +/// rejected as malformed wire input rather than recursed into, +/// so a hostile client can't blow the call stack with +/// `list(list(list(...)))` before schema validation. +/// +/// `None` (oneof unset on the wire) is rejected — a where-clause +/// operand is always concrete; empty where-clauses are expressed +/// by an empty `where_clauses` field at the request level, not by +/// sending an empty `DocumentFieldValue`. +pub(crate) fn value_from_proto(value: ProtoDocumentFieldValue) -> Result { + value_from_proto_at_depth(value, 0) +} + +/// Recursion-bounded form of [`value_from_proto`]. `depth = 0` is +/// the request-level operand; the only legal child shape is a +/// flat list (`depth = 1` for `IN` / `BETWEEN*` candidates), so a +/// `list` encountered at `depth >= 1` is wire-malformed. +fn value_from_proto_at_depth( + value: ProtoDocumentFieldValue, + depth: u8, +) -> Result { + let variant = value.variant.ok_or_else(|| { + DecodeError::InvalidArgument( + "DocumentFieldValue has no variant set; a where-clause operand must \ + be a concrete value" + .to_string(), + ) + })?; + Ok(match variant { + document_field_value::Variant::BoolValue(b) => Value::Bool(b), + document_field_value::Variant::Int64Value(i) => Value::I64(i), + document_field_value::Variant::Uint64Value(u) => Value::U64(u), + document_field_value::Variant::DoubleValue(f) => Value::Float(f), + document_field_value::Variant::Text(s) => Value::Text(s), + document_field_value::Variant::BytesValue(b) => Value::Bytes(b), + document_field_value::Variant::List(list) => { + if depth >= 1 { + return Err(DecodeError::InvalidArgument( + "nested DocumentFieldValue.list is not supported; the v1 \ + query surface accepts at most one level of nesting \ + (`IN` / `BETWEEN*` candidate lists of scalars)" + .to_string(), + )); + } + Value::Array( + list.values + .into_iter() + .map(|v| value_from_proto_at_depth(v, depth + 1)) + .collect::, _>>()?, + ) + } + // The bool payload is a placeholder — picking the + // `null_value` variant means "this operand is null" and + // the bool itself is ignored. See the proto-side comment + // on the field for the rationale. + document_field_value::Variant::NullValue(_) => Value::Null, + }) +} + +/// Map a wire [`ProtoWhereClause`] onto drive's structured +/// [`WhereClause`]. Errors surface as +/// [`DecodeError::InvalidArgument`] for both operator-discriminant +/// and value-shape failures. +pub(crate) fn where_clause_from_proto( + clause: ProtoWhereClause, +) -> Result { + let operator = where_operator_from_proto(clause.operator)?; + let value = clause.value.ok_or_else(|| { + DecodeError::InvalidArgument(format!( + "WhereClause on field '{}' has no value set; every clause must carry a \ + concrete `DocumentFieldValue`", + clause.field + )) + })?; + let value = value_from_proto(value)?; + Ok(WhereClause { + field: clause.field, + operator, + value, + }) +} + +/// Plural form of `where_clause_from_proto` for the request-level +/// `repeated WhereClause` field. Returns an error on the first +/// malformed clause. +pub fn where_clauses_from_proto( + clauses: Vec, +) -> Result, DecodeError> { + clauses.into_iter().map(where_clause_from_proto).collect() +} + +/// Map a wire [`ProtoOrderClause`] onto drive's [`OrderClause`]. +/// +/// The `target` oneof currently has two variants on the wire: +/// `field` (plain column name — evaluated today) and `aggregate` +/// (aggregate function applied to a field — wire-only, rejected +/// with [`DecodeError::Unsupported`]). Unset (`None`) is rejected +/// as malformed wire input. +pub(crate) fn order_clause_from_proto( + clause: ProtoOrderClause, +) -> Result { + let ascending = clause.ascending; + match clause.target { + Some(order_clause::Target::Field(field)) => Ok(OrderClause { field, ascending }), + Some(order_clause::Target::Aggregate(_)) => Err(DecodeError::Unsupported( + "ORDER BY on aggregate keys is not yet implemented".to_string(), + )), + None => Err(DecodeError::InvalidArgument( + "OrderClause has no target set; every clause must carry either a \ + `field` (plain column name) or an `aggregate` (aggregate-function \ + ordering target)" + .to_string(), + )), + } +} + +/// Plural form of `order_clause_from_proto` for the request-level +/// `repeated OrderClause` field. Returns the first error +/// encountered. +pub fn order_clauses_from_proto( + clauses: Vec, +) -> Result, DecodeError> { + clauses.into_iter().map(order_clause_from_proto).collect() +} + +/// Map a wire [`having_aggregate::Function`] discriminant onto +/// drive's [`HavingAggregateFunction`]. Unknown discriminants are +/// wire-level garbage (no future protocol value would map a +/// malformed integer to a valid behavior), so they surface as +/// [`DecodeError::InvalidArgument`]. +fn having_function_from_proto(function: i32) -> Result { + let proto = having_aggregate::Function::try_from(function).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown HavingAggregate.Function discriminant: {} (valid values: 0..=2, see \ + `get_documents_request::having_aggregate::Function`)", + function + )) + })?; + Ok(match proto { + having_aggregate::Function::Count => HavingAggregateFunction::Count, + having_aggregate::Function::Sum => HavingAggregateFunction::Sum, + having_aggregate::Function::Avg => HavingAggregateFunction::Avg, + }) +} + +/// Map a wire [`having_clause::Operator`] discriminant onto +/// drive's [`HavingOperator`]. Same error contract as +/// [`having_function_from_proto`]. +fn having_operator_from_proto(operator: i32) -> Result { + let proto = having_clause::Operator::try_from(operator).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown HavingClause.Operator discriminant: {} (valid values: 0..=10, see \ + `get_documents_request::having_clause::Operator`)", + operator + )) + })?; + Ok(match proto { + having_clause::Operator::Equal => HavingOperator::Equal, + having_clause::Operator::NotEqual => HavingOperator::NotEqual, + having_clause::Operator::GreaterThan => HavingOperator::GreaterThan, + having_clause::Operator::GreaterThanOrEquals => HavingOperator::GreaterThanOrEquals, + having_clause::Operator::LessThan => HavingOperator::LessThan, + having_clause::Operator::LessThanOrEquals => HavingOperator::LessThanOrEquals, + having_clause::Operator::Between => HavingOperator::Between, + having_clause::Operator::BetweenExcludeBounds => HavingOperator::BetweenExcludeBounds, + having_clause::Operator::BetweenExcludeLeft => HavingOperator::BetweenExcludeLeft, + having_clause::Operator::BetweenExcludeRight => HavingOperator::BetweenExcludeRight, + having_clause::Operator::In => HavingOperator::In, + }) +} + +/// Map a wire [`ProtoHavingAggregate`] onto drive's +/// [`HavingAggregate`]. The aggregate-function ↔ field +/// consistency check (`field` required for everything except +/// `Count`) runs inside the evaluator when HAVING execution +/// lands; the converter only enforces that the proto shape is +/// well-formed. +fn having_aggregate_from_proto( + aggregate: ProtoHavingAggregate, +) -> Result { + Ok(HavingAggregate { + function: having_function_from_proto(aggregate.function)?, + field: aggregate.field, + }) +} + +/// Map a wire [`ProtoHavingClause`] onto drive's structured +/// [`HavingClause`]. Errors surface as +/// [`DecodeError::InvalidArgument`] for any wire-level +/// malformation: unknown discriminant on the aggregate function or +/// operator; missing aggregate; missing right operand (oneof unset +/// on the wire); inner value-shape failures on the literal-value +/// branch. +/// +/// `HAVING` is a boolean per-group predicate and nothing else, so the +/// wire's `right` oneof has exactly one arm and this function has +/// exactly one thing to decode. Cross-group ranking is expressed with +/// SQL's own ordering surface — `ORDER BY DESC +/// LIMIT n [OFFSET m]` — which arrives as an `OrderClause` and never +/// reaches here. +pub(crate) fn having_clause_from_proto( + clause: ProtoHavingClause, +) -> Result { + let aggregate = clause.aggregate.ok_or_else(|| { + DecodeError::InvalidArgument( + "HavingClause has no aggregate set; every clause must carry an \ + aggregate function + field operand" + .to_string(), + ) + })?; + let aggregate = having_aggregate_from_proto(aggregate)?; + let operator = having_operator_from_proto(clause.operator)?; + let right = clause.right.ok_or_else(|| { + DecodeError::InvalidArgument( + "HavingClause has no right operand set; every clause must carry a \ + concrete `DocumentFieldValue` (`right.value`)" + .to_string(), + ) + })?; + let right = match right { + having_clause::Right::Value(v) => HavingRightOperand::Value(value_from_proto(v)?), + }; + Ok(HavingClause { + aggregate, + operator, + right, + }) +} + +/// Plural form of `having_clause_from_proto` for the request- +/// level `repeated HavingClause` field. Returns an error on the +/// first malformed clause. +pub fn having_clauses_from_proto( + clauses: Vec, +) -> Result, DecodeError> { + clauses.into_iter().map(having_clause_from_proto).collect() +} + +/// Map a wire [`select::Function`] discriminant onto drive's +/// [`SelectFunction`]. Unknown discriminants are wire-level +/// garbage (no future protocol value would map a malformed +/// integer to a valid behavior), so they surface as +/// [`DecodeError::InvalidArgument`]. +fn select_function_from_proto(function: i32) -> Result { + let proto = select::Function::try_from(function).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown Select.Function discriminant: {} (valid values: 0..=5, see \ + `get_documents_request::get_documents_request_v1::select::Function`)", + function + )) + })?; + Ok(match proto { + select::Function::Documents => SelectFunction::Documents, + select::Function::Count => SelectFunction::Count, + select::Function::Sum => SelectFunction::Sum, + select::Function::Avg => SelectFunction::Avg, + select::Function::Min => SelectFunction::Min, + select::Function::Max => SelectFunction::Max, + }) +} + +/// Map a wire [`ProtoSelect`] onto drive's [`SelectProjection`]. +/// An unset `select` field on the request decodes as the proto- +/// default `Select { function: DOCUMENTS, field: "" }`, which +/// maps to [`SelectProjection::documents()`] — keeps callers that +/// don't set the field on the v0-style document-fetch path. +/// +/// Per-function field constraints (e.g. `DOCUMENTS` must have +/// empty `field`, `SUM`/`AVG` require non-empty) are checked at +/// routing time by the server's `validate_and_route`, not here, so +/// the converter only enforces well-formed proto. +pub fn select_from_proto(select: ProtoSelect) -> Result { + Ok(SelectProjection { + function: select_function_from_proto(select.function)?, + field: select.field, + }) +} From d3f66179601f999f32fb2d6ebceca01698884fd1 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 22 Aug 2026 12:20:37 -0500 Subject: [PATCH 2/8] refactor(sdk): split consensus label validation from client username policy is_consensus_valid_label matches exactly the DPNS contract's label schema pattern (consecutive hyphens allowed); is_valid_username is recomposed as that pattern plus the stricter client-side consecutive-hyphen rejection. Its acceptance set is unchanged - the pre-existing test vectors pass as-is - but the consensus check is now available on its own so document builders cannot reject labels the contract accepts. --- .../src/dpns_usernames.rs | 66 ++++++++----------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/packages/dash-platform-queries/src/dpns_usernames.rs b/packages/dash-platform-queries/src/dpns_usernames.rs index 3f452519b29..bb001560322 100644 --- a/packages/dash-platform-queries/src/dpns_usernames.rs +++ b/packages/dash-platform-queries/src/dpns_usernames.rs @@ -18,15 +18,34 @@ pub fn convert_to_homograph_safe_chars(input: &str) -> String { .collect() } -/// Check if a username is valid according to DPNS rules -/// -/// A username is valid if: -/// - It's between 3 and 63 characters long -/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) -/// - It contains only alphanumeric characters and hyphens -/// - It doesn't have consecutive hyphens (enforced by the pattern) +/// Check whether a label satisfies the DPNS contract's `label` schema +/// pattern — exactly what consensus enforces, nothing stricter. /// /// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` +/// (3-63 characters, alphanumeric and hyphens, alphanumeric at both ends; +/// consecutive hyphens ARE allowed by consensus). +pub fn is_consensus_valid_label(label: &str) -> bool { + if label.len() < 3 || label.len() > 63 { + return false; + } + let chars: Vec = label.chars().collect(); + if !chars[0].is_ascii_alphanumeric() || !chars[chars.len() - 1].is_ascii_alphanumeric() { + return false; + } + chars[1..chars.len() - 1] + .iter() + .all(|&ch| ch.is_ascii_alphanumeric() || ch == '-') +} + +/// Check if a username is valid according to this crate's recommended +/// client-side policy: the consensus pattern plus a stricter rejection of +/// consecutive hyphens. +/// +/// This is deliberately narrower than [`is_consensus_valid_label`] — a name +/// like `ab--cd` is consensus-valid but rejected here, matching the +/// pre-existing policy of the mobile SDK FFI and wasm-sdk gates. Callers +/// that must accept every consensus-valid label should use +/// [`is_consensus_valid_label`] instead. /// /// # Arguments /// @@ -36,38 +55,7 @@ pub fn convert_to_homograph_safe_chars(input: &str) -> String { /// /// Returns `true` if the username is valid, `false` otherwise pub fn is_valid_username(label: &str) -> bool { - // Check length - if label.len() < 3 || label.len() > 63 { - return false; - } - - let chars: Vec = label.chars().collect(); - - // Check first character (must be alphanumeric) - if !chars[0].is_ascii_alphanumeric() { - return false; - } - - // Check last character (must be alphanumeric) - if !chars[chars.len() - 1].is_ascii_alphanumeric() { - return false; - } - - // Check middle characters (can be alphanumeric or hyphen) - for &ch in &chars[1..chars.len() - 1] { - if !ch.is_ascii_alphanumeric() && ch != '-' { - return false; - } - } - - // Additional check: no consecutive hyphens (good practice) - for i in 0..chars.len() - 1 { - if chars[i] == '-' && chars[i + 1] == '-' { - return false; - } - } - - true + is_consensus_valid_label(label) && !label.contains("--") } /// Check if a username is contested (requires masternode voting) From 9d15113d8d1da917c5318436122b81d21dd00676 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 22 Aug 2026 12:25:14 -0500 Subject: [PATCH 3/8] refactor(sdk): separate DPNS and DashPay document assembly from networked flows register_dpns_name and create_contact_request were interleaving document assembly (id derivation, salted-domain-hash commitment, property maps, size validation) with fetching, ECDH, and broadcasting. The assembly halves become pure functions - build_dpns_preorder_and_domain_documents and build_contact_request_document - that take caller-supplied entropy/salt/ciphertexts and touch no network or randomness. The networked flows now call them; ids, properties, size-validation bounds, and error messages are unchanged. One addition beyond the extraction: the DPNS builder validates the label against the consensus pattern (is_consensus_valid_label) before assembling. The previous flow did no label validation locally and let the network reject invalid labels; failing locally with a clear message is strictly earlier, and using the consensus pattern (not the stricter client policy) means the builder cannot reject labels the contract accepts. --- .../src/platform/dashpay/contact_request.rs | 234 ++++++++++------ packages/rs-sdk/src/platform/dashpay/mod.rs | 8 +- .../rs-sdk/src/platform/dpns_usernames/mod.rs | 251 ++++++++++++------ 3 files changed, 325 insertions(+), 168 deletions(-) diff --git a/packages/rs-sdk/src/platform/dashpay/contact_request.rs b/packages/rs-sdk/src/platform/dashpay/contact_request.rs index d595faaaed7..e9b10d77778 100644 --- a/packages/rs-sdk/src/platform/dashpay/contact_request.rs +++ b/packages/rs-sdk/src/platform/dashpay/contact_request.rs @@ -23,6 +23,134 @@ use platform_encryption::{ }; use std::collections::BTreeMap; +use dpp::data_contract::DataContract; + +/// 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. +#[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, + /// Optional encrypted account label: 48-80 bytes + /// (16-byte IV + 32-64 bytes of encrypted data) + pub encrypted_account_label: Option>, + /// Optional auto-accept proof (38-102 bytes) - not encrypted + pub auto_accept_proof: Option>, + /// 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], +} + +/// 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::Generic(format!( + "autoAcceptProof must be 38-102 bytes, got {}", + proof.len() + ))); + } + Ok(()) +} + +/// Build the id and property map of a DIP-15 `contactRequest` document from +/// already-derived crypto material. +/// +/// This is the pure document-assembly half of [`Sdk::create_contact_request`]: +/// the document id derives from `params.entropy`, and the property map +/// carries exactly the fields the DashPay contract defines (`toUserId`, +/// `encryptedPublicKey`, `senderKeyIndex`, `recipientKeyIndex`, +/// `accountReference`, plus the optional `encryptedAccountLabel` and +/// `autoAcceptProof`). +/// +/// Returns `(document_id, properties)`. +pub fn build_contact_request_document( + contract: &DataContract, + params: ContactRequestDocumentParams, +) -> Result<(Identifier, BTreeMap), 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::Generic(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::Generic(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::Generic("DashPay contactRequest document type not found".to_string()) + })?; + + let document_id = Document::generate_document_id_v0( + &contract.id(), + ¶ms.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_id, properties)) +} + /// ECDH provider for contact request encryption /// /// Supports two modes: @@ -259,14 +387,11 @@ impl Sdk { H: FnOnce(u32) -> Hut, Hut: std::future::Future, Error>>, { - // Validate auto accept proof size if provided + // Validate auto accept proof size if provided. The builder + // validates again, but checking here first keeps the failure local — + // before the recipient fetch and ECDH work below. if let Some(ref proof) = input.auto_accept_proof { - if proof.len() < 38 || proof.len() > 102 { - return Err(Error::Generic(format!( - "autoAcceptProof must be 38-102 bytes, got {}", - proof.len() - ))); - } + validate_auto_accept_proof(proof)?; } // Fetch recipient identity if only ID was provided @@ -362,90 +487,45 @@ impl Sdk { let mut xpub_iv = [0u8; 16]; rng.fill_bytes(&mut xpub_iv); - // Encrypt the extended public key (includes IV prepended) + // Encrypt the extended public key (includes IV prepended). The + // builder rejects any ciphertext that isn't exactly 96 bytes + // (16-byte IV + 80-byte encrypted data). let encrypted_public_key = encrypt_extended_public_key(&shared_key, &xpub_iv, &extended_public_key); - // Validate encrypted public key size (must be exactly 96 bytes: 16-byte IV + 80-byte encrypted data) - if encrypted_public_key.len() != 96 { - return Err(Error::Generic(format!( - "Encrypted public key size mismatch: expected 96 bytes, got {}", - encrypted_public_key.len() - ))); - } - - // Encrypt the account label if provided (includes IV prepended) - let encrypted_account_label = if let Some(ref label) = input.account_label { + // Encrypt the account label if provided (includes IV prepended). The + // builder rejects any ciphertext outside 48-80 bytes + // (16-byte IV + 32-64 byte encrypted data). + let encrypted_account_label = input.account_label.as_ref().map(|label| { let mut label_iv = [0u8; 16]; rng.fill_bytes(&mut label_iv); - let encrypted = encrypt_account_label(&shared_key, &label_iv, label); - - // Validate encrypted label size (48-80 bytes: 16-byte IV + 32-64 byte encrypted data) - if encrypted.len() < 48 || encrypted.len() > 80 { - return Err(Error::Generic(format!( - "Encrypted account label size out of range: expected 48-80 bytes, got {}", - encrypted.len() - ))); - } - Some(encrypted) - } else { - None - }; + encrypt_account_label(&shared_key, &label_iv, label) + }); // Fetch DashPay contract let dashpay_contract = self.fetch_dashpay_contract().await?; - // Get contactRequest document type - let contact_request_document_type = dashpay_contract - .document_type_for_name("contactRequest") - .map_err(|_| { - Error::Generic("DashPay contactRequest document type not found".to_string()) - })?; - // Generate entropy for document ID let mut rng = StdRng::from_entropy(); let entropy = Bytes32::random_with_rng(&mut rng); - // Generate document ID + // Assemble the document in the pure builder above, keeping document + // assembly separate from this networked flow. let sender_id = input.sender_identity.id().to_owned(); - let document_id = Document::generate_document_id_v0( - &dashpay_contract.id(), - &sender_id, - contact_request_document_type.name(), - entropy.as_slice(), - ); - - // Build document properties - let mut properties = BTreeMap::new(); - let recipient_id = recipient_identity.id().to_owned(); - properties.insert( - "toUserId".to_string(), - Value::Identifier(recipient_id.to_buffer()), - ); - properties.insert( - "encryptedPublicKey".to_string(), - Value::Bytes(encrypted_public_key), - ); - properties.insert( - "senderKeyIndex".to_string(), - Value::U32(input.sender_key_index), - ); - properties.insert( - "recipientKeyIndex".to_string(), - Value::U32(input.recipient_key_index), - ); - properties.insert( - "accountReference".to_string(), - Value::U32(input.account_reference), - ); - - // Add optional fields - if let Some(label) = encrypted_account_label { - properties.insert("encryptedAccountLabel".to_string(), Value::Bytes(label)); - } - if let Some(proof) = input.auto_accept_proof { - properties.insert("autoAcceptProof".to_string(), Value::Bytes(proof)); - } + let (document_id, properties) = build_contact_request_document( + &dashpay_contract, + ContactRequestDocumentParams { + sender_id, + recipient_id: recipient_identity.id().to_owned(), + sender_key_index: input.sender_key_index, + recipient_key_index: input.recipient_key_index, + account_reference: input.account_reference, + encrypted_public_key, + encrypted_account_label, + auto_accept_proof: input.auto_accept_proof, + entropy: entropy.0, + }, + )?; // Return the essential fields for the contact request, including the // entropy that derived `document_id` so the broadcast path can reuse it. diff --git a/packages/rs-sdk/src/platform/dashpay/mod.rs b/packages/rs-sdk/src/platform/dashpay/mod.rs index 182edd8854b..1991150d643 100644 --- a/packages/rs-sdk/src/platform/dashpay/mod.rs +++ b/packages/rs-sdk/src/platform/dashpay/mod.rs @@ -7,9 +7,11 @@ mod contact_request; mod contact_request_queries; pub use contact_request::{ - recipient_key_purpose_is_acceptable_on_receive, recipient_key_purpose_is_valid, - sender_key_purpose_is_acceptable_on_receive, ContactRequestInput, ContactRequestResult, - EcdhProvider, RecipientIdentity, SendContactRequestInput, SendContactRequestResult, + build_contact_request_document, recipient_key_purpose_is_acceptable_on_receive, + recipient_key_purpose_is_valid, sender_key_purpose_is_acceptable_on_receive, + validate_auto_accept_proof, ContactRequestDocumentParams, ContactRequestInput, + ContactRequestResult, EcdhProvider, RecipientIdentity, SendContactRequestInput, + SendContactRequestResult, }; pub use contact_request_queries::ContactRequestDocuments; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 6de3e2950d1..eacec643775 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -11,10 +11,12 @@ use crate::platform::transition::put_document::PutDocument; use crate::platform::{Document, Fetch, FetchMany}; use crate::{Error, Sdk}; use dash_context_provider::ContextProvider; +use dash_platform_queries::dpns_usernames::is_consensus_valid_label; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::DataContract; use dpp::document::{DocumentV0, DocumentV0Getters}; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::signer::Signer; @@ -53,6 +55,157 @@ fn hash_double(data: Vec) -> [u8; 32] { hash.to_byte_array() } +/// Build the DPNS `preorder` and `domain` documents that register +/// `label`.dash for `identity_id`, exactly as platform consensus expects +/// them. +/// +/// This is the pure document-assembly half of [`Sdk::register_dpns_name`]: +/// no networking, and no randomness — the caller supplies the `entropy` +/// that derives both document ids (the same entropy must later be attached +/// to both create transitions) and the preorder `salt`, whose double-SHA256 +/// over `salt ‖ ".dash"` becomes the preorder's +/// `saltedDomainHash`. +/// +/// The `label` must satisfy [`is_consensus_valid_label`]; the raw label is stored +/// in the domain document's `label` property while its +/// [homograph-safe](convert_to_homograph_safe_chars) form is stored in +/// `normalizedLabel`. +/// +/// # Salt secrecy and reveal order +/// +/// The preorder/domain split is DPNS's front-running protection: the +/// preorder commits to `saltedDomainHash` without revealing which name +/// is being registered, and only the later domain document discloses +/// the `label` and the `preorderSalt` that tie it to the commitment. +/// That protection holds only if the caller upholds what +/// [`Sdk::register_dpns_name`] does automatically: +/// +/// - generate a **fresh 32-byte salt from a CSPRNG** for every +/// registration attempt (the SDK draws it from +/// `StdRng::from_entropy()`). A reused or predictable salt lets an +/// observer precompute `sha256d(salt ‖ ".dash")` for +/// candidate labels and identify — then front-run — the name from +/// the preorder alone; +/// - keep the salt, the label, and the assembled domain document +/// **private until the preorder create transition is confirmed** +/// (the SDK submits the preorder and waits for its response before +/// broadcasting the domain document). Revealing them earlier +/// discloses the name while it is still unclaimed, defeating the +/// commitment. +/// +/// Callers driving their own flow inherit both obligations — this +/// builder takes `salt` as an argument precisely because it has no +/// randomness of its own and cannot enforce either one. +/// +/// Returns `(preorder_document, domain_document)`. +pub fn build_dpns_preorder_and_domain_documents( + contract: &DataContract, + identity_id: Identifier, + label: &str, + entropy: [u8; 32], + salt: [u8; 32], +) -> Result<(Document, Document), Error> { + if !is_consensus_valid_label(label) { + return Err(Error::Generic(format!( + "Invalid DPNS label \"{label}\": must be 3-63 characters, alphanumeric and hyphens \ + only, starting and ending with an alphanumeric character" + ))); + } + + let preorder_document_type = contract + .document_type_for_name("preorder") + .map_err(|_| Error::Generic("DPNS preorder document type not found".to_string()))?; + + let domain_document_type = contract + .document_type_for_name("domain") + .map_err(|_| Error::Generic("DPNS domain document type not found".to_string()))?; + + let preorder_id = Document::generate_document_id_v0( + &contract.id(), + &identity_id, + preorder_document_type.name(), + entropy.as_slice(), + ); + let domain_id = Document::generate_document_id_v0( + &contract.id(), + &identity_id, + domain_document_type.name(), + entropy.as_slice(), + ); + + // Create salted domain hash for preorder + let normalized_label = convert_to_homograph_safe_chars(label); + let mut salted_domain_buffer: Vec = vec![]; + salted_domain_buffer.extend(salt); + salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); + let salted_domain_hash = hash_double(salted_domain_buffer); + + let preorder_document = Document::V0(DocumentV0 { + id: preorder_id, + owner_id: identity_id, + properties: BTreeMap::from([( + "saltedDomainHash".to_string(), + Value::Bytes32(salted_domain_hash), + )]), + 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, + }); + + let domain_document = Document::V0(DocumentV0 { + id: domain_id, + owner_id: identity_id, + properties: BTreeMap::from([ + ( + "parentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ( + "normalizedParentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ("label".to_string(), Value::Text(label.to_string())), + ("normalizedLabel".to_string(), Value::Text(normalized_label)), + ("preorderSalt".to_string(), Value::Bytes32(salt)), + ( + "records".to_string(), + Value::Map(vec![( + Value::Text("identity".to_string()), + Value::Identifier(identity_id.to_buffer()), + )]), + ), + ( + "subdomainRules".to_string(), + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false), + )]), + ), + ]), + 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, + }); + + Ok((preorder_document, domain_document)) +} + /// Callback type for preorder document pub type PreorderCallback = Box; @@ -164,95 +317,17 @@ impl Sdk { let entropy = Bytes32::random_with_rng(&mut rng); let salt: [u8; 32] = rng.gen(); - // Generate document IDs - let identity_id = input.identity.id().to_owned(); - let preorder_id = Document::generate_document_id_v0( - &dpns_contract.id(), - &identity_id, - preorder_document_type.name(), - entropy.as_slice(), - ); - let domain_id = Document::generate_document_id_v0( - &dpns_contract.id(), - &identity_id, - domain_document_type.name(), - entropy.as_slice(), - ); - - // Create salted domain hash for preorder + // Assemble both documents in the pure builder above, keeping + // document assembly separate from this networked flow. + let (preorder_document, domain_document) = build_dpns_preorder_and_domain_documents( + &dpns_contract, + input.identity.id().to_owned(), + &input.label, + entropy.0, + salt, + )?; + let normalized_label = convert_to_homograph_safe_chars(&input.label); - let mut salted_domain_buffer: Vec = vec![]; - salted_domain_buffer.extend(salt); - salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); - let salted_domain_hash = hash_double(salted_domain_buffer); - - // Create preorder document - let preorder_document = Document::V0(DocumentV0 { - id: preorder_id, - owner_id: identity_id, - properties: BTreeMap::from([( - "saltedDomainHash".to_string(), - Value::Bytes32(salted_domain_hash), - )]), - 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, - }); - - // Create domain document - let domain_document = Document::V0(DocumentV0 { - id: domain_id, - owner_id: identity_id, - properties: BTreeMap::from([ - ( - "parentDomainName".to_string(), - Value::Text("dash".to_string()), - ), - ( - "normalizedParentDomainName".to_string(), - Value::Text("dash".to_string()), - ), - ("label".to_string(), Value::Text(input.label.clone())), - ( - "normalizedLabel".to_string(), - Value::Text(normalized_label.clone()), - ), - ("preorderSalt".to_string(), Value::Bytes32(salt)), - ( - "records".to_string(), - Value::Map(vec![( - Value::Text("identity".to_string()), - Value::Identifier(identity_id.to_buffer()), - )]), - ), - ( - "subdomainRules".to_string(), - Value::Map(vec![( - Value::Text("allowSubdomains".to_string()), - Value::Bool(false), - )]), - ), - ]), - 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, - }); // Submit preorder document first let platform_preorder_document = preorder_document From c02bbcf3c787e87d698efa34dc37591ffaccb87d Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 22 Aug 2026 12:26:36 -0500 Subject: [PATCH 4/8] refactor(sdk): move pure DPNS and DashPay document builders into dash-platform-queries File move of the pure builders introduced in the previous commit, unchanged except for the error type: they now return dash_platform_queries::Error::InvalidInput, which dash-sdk maps back to Error::Generic with identical messages, so the SDK surface is byte-for-byte the same. rs-sdk re-exports the builders at their previous paths. This makes the document-assembly half of DPNS registration and DashPay contact requests reachable without the SDK's transport stack; crypto material and randomness stay with the caller. --- packages/dash-platform-queries/src/dashpay.rs | 143 +++++++++++++++ .../src/dpns_usernames.rs | 173 +++++++++++++++++- packages/dash-platform-queries/src/error.rs | 6 + packages/dash-platform-queries/src/lib.rs | 1 + packages/rs-sdk/src/error.rs | 3 + .../src/platform/dashpay/contact_request.rs | 142 +------------- packages/rs-sdk/src/platform/dashpay/mod.rs | 11 +- .../rs-sdk/src/platform/dpns_usernames/mod.rs | 172 +---------------- 8 files changed, 343 insertions(+), 308 deletions(-) create mode 100644 packages/dash-platform-queries/src/dashpay.rs diff --git a/packages/dash-platform-queries/src/dashpay.rs b/packages/dash-platform-queries/src/dashpay.rs new file mode 100644 index 00000000000..37d9bbf5fd5 --- /dev/null +++ b/packages/dash-platform-queries/src/dashpay.rs @@ -0,0 +1,143 @@ +//! 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; +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, + /// Optional encrypted account label: 48-80 bytes + /// (16-byte IV + 32-64 bytes of encrypted data) + pub encrypted_account_label: Option>, + /// Optional auto-accept proof (38-102 bytes) - not encrypted + pub auto_accept_proof: Option>, + /// 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], +} + +/// 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 the id and property map of 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`, and the property map carries exactly the fields the +/// DashPay contract defines (`toUserId`, `encryptedPublicKey`, +/// `senderKeyIndex`, `recipientKeyIndex`, `accountReference`, plus the +/// optional `encryptedAccountLabel` and `autoAcceptProof`). +/// +/// Returns `(document_id, properties)`. +pub fn build_contact_request_document( + contract: &DataContract, + params: ContactRequestDocumentParams, +) -> Result<(Identifier, BTreeMap), 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(), + ¶ms.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_id, properties)) +} diff --git a/packages/dash-platform-queries/src/dpns_usernames.rs b/packages/dash-platform-queries/src/dpns_usernames.rs index bb001560322..585ba8235b6 100644 --- a/packages/dash-platform-queries/src/dpns_usernames.rs +++ b/packages/dash-platform-queries/src/dpns_usernames.rs @@ -1,8 +1,177 @@ //! Transport-free DPNS username helpers. //! //! The Sdk-bound DPNS surface (registration, availability checks, name -//! resolution) lives in `dash-sdk`; these free functions are pure string -//! validation/normalization shared with embedders. +//! resolution) lives in `dash-sdk`; the free functions here are the pure +//! pieces shared with embedders: string validation/normalization and the +//! preorder/domain document assembly used to register a name. + +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; + +/// Hash a buffer twice using SHA256 (double SHA256) +fn hash_double(data: Vec) -> [u8; 32] { + use dpp::dashcore::hashes::{sha256d, Hash}; + // sha256d already does double SHA256 + let hash = sha256d::Hash::hash(&data); + hash.to_byte_array() +} + +/// Build the DPNS `preorder` and `domain` documents that register +/// `label`.dash for `identity_id`, exactly as platform consensus expects +/// them. +/// +/// This is the pure document-assembly half of `dash-sdk`'s +/// `register_dpns_name`: no networking, and no randomness — the caller +/// supplies the `entropy` that derives both document ids (the same entropy +/// must later be attached to both create transitions) and the preorder +/// `salt`, whose double-SHA256 over `salt ‖ ".dash"` +/// becomes the preorder's `saltedDomainHash`. +/// +/// The `label` must satisfy [`is_consensus_valid_label`]; the raw label is stored +/// in the domain document's `label` property while its +/// [homograph-safe](convert_to_homograph_safe_chars) form is stored in +/// `normalizedLabel`. +/// +/// # Salt secrecy and reveal order +/// +/// The preorder/domain split is DPNS's front-running protection: the +/// preorder commits to `saltedDomainHash` without revealing which name +/// is being registered, and only the later domain document discloses +/// the `label` and the `preorderSalt` that tie it to the commitment. +/// That protection holds only if the caller upholds what the networked +/// SDK (`dash-sdk`'s `register_dpns_name`) does automatically: +/// +/// - generate a **fresh 32-byte salt from a CSPRNG** for every +/// registration attempt (the SDK draws it from +/// `StdRng::from_entropy()`). A reused or predictable salt lets an +/// observer precompute `sha256d(salt ‖ ".dash")` for +/// candidate labels and identify — then front-run — the name from +/// the preorder alone; +/// - keep the salt, the label, and the assembled domain document +/// **private until the preorder create transition is confirmed** +/// (the SDK submits the preorder and waits for its response before +/// broadcasting the domain document). Revealing them earlier +/// discloses the name while it is still unclaimed, defeating the +/// commitment. +/// +/// Embedders driving their own transport inherit both obligations — +/// this builder takes `salt` as an argument precisely because it has +/// no randomness of its own and cannot enforce either one. +/// +/// Returns `(preorder_document, domain_document)`. +pub fn build_dpns_preorder_and_domain_documents( + contract: &DataContract, + identity_id: Identifier, + label: &str, + entropy: [u8; 32], + salt: [u8; 32], +) -> Result<(Document, Document), Error> { + if !is_consensus_valid_label(label) { + return Err(Error::InvalidInput(format!( + "Invalid DPNS label \"{label}\": must be 3-63 characters, alphanumeric and hyphens \ + only, starting and ending with an alphanumeric character" + ))); + } + + let preorder_document_type = contract + .document_type_for_name("preorder") + .map_err(|_| Error::InvalidInput("DPNS preorder document type not found".to_string()))?; + + let domain_document_type = contract + .document_type_for_name("domain") + .map_err(|_| Error::InvalidInput("DPNS domain document type not found".to_string()))?; + + let preorder_id = Document::generate_document_id_v0( + &contract.id(), + &identity_id, + preorder_document_type.name(), + entropy.as_slice(), + ); + let domain_id = Document::generate_document_id_v0( + &contract.id(), + &identity_id, + domain_document_type.name(), + entropy.as_slice(), + ); + + // Create salted domain hash for preorder + let normalized_label = convert_to_homograph_safe_chars(label); + let mut salted_domain_buffer: Vec = vec![]; + salted_domain_buffer.extend(salt); + salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); + let salted_domain_hash = hash_double(salted_domain_buffer); + + let preorder_document = Document::V0(DocumentV0 { + id: preorder_id, + owner_id: identity_id, + properties: BTreeMap::from([( + "saltedDomainHash".to_string(), + Value::Bytes32(salted_domain_hash), + )]), + 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, + }); + + let domain_document = Document::V0(DocumentV0 { + id: domain_id, + owner_id: identity_id, + properties: BTreeMap::from([ + ( + "parentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ( + "normalizedParentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ("label".to_string(), Value::Text(label.to_string())), + ("normalizedLabel".to_string(), Value::Text(normalized_label)), + ("preorderSalt".to_string(), Value::Bytes32(salt)), + ( + "records".to_string(), + Value::Map(vec![( + Value::Text("identity".to_string()), + Value::Identifier(identity_id.to_buffer()), + )]), + ), + ( + "subdomainRules".to_string(), + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false), + )]), + ), + ]), + 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, + }); + + Ok((preorder_document, domain_document)) +} /// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' /// with '0', '1', and '1' respectively to prevent homograph attacks diff --git a/packages/dash-platform-queries/src/error.rs b/packages/dash-platform-queries/src/error.rs index 0d8727763ce..ee42f67b7e2 100644 --- a/packages/dash-platform-queries/src/error.rs +++ b/packages/dash-platform-queries/src/error.rs @@ -15,6 +15,12 @@ pub enum Error { /// Query is not configured properly for the target platform version #[error("SDK misconfigured: {0}")] Config(String), + /// Input to a document builder failed validation (bad label, wrong + /// ciphertext length, unknown document type, ...). `dash-sdk` maps this + /// to its `Error::Generic`, preserving the messages these checks + /// produced before they moved here. + #[error("{0}")] + InvalidInput(String), /// Drive error #[error("Drive error: {0}")] Drive(#[from] drive::error::Error), diff --git a/packages/dash-platform-queries/src/lib.rs b/packages/dash-platform-queries/src/lib.rs index 9da47cf6435..29b71b8f434 100644 --- a/packages/dash-platform-queries/src/lib.rs +++ b/packages/dash-platform-queries/src/lib.rs @@ -13,6 +13,7 @@ #![allow(clippy::result_large_err)] pub mod block_info_from_metadata; +pub mod dashpay; pub mod documents; pub mod dpns_usernames; pub mod error; diff --git a/packages/rs-sdk/src/error.rs b/packages/rs-sdk/src/error.rs index cc8309ebcd4..89ade69e741 100644 --- a/packages/rs-sdk/src/error.rs +++ b/packages/rs-sdk/src/error.rs @@ -137,6 +137,9 @@ impl From for Error { fn from(value: dash_platform_queries::Error) -> Self { match value { dash_platform_queries::Error::Config(msg) => Self::Config(msg), + // Builder input validation moved to the query core keeps surfacing + // as Generic with the exact messages it produced inside this crate. + dash_platform_queries::Error::InvalidInput(msg) => Self::Generic(msg), dash_platform_queries::Error::Drive(e) => Self::Drive(e), dash_platform_queries::Error::Protocol(e) => Self::Protocol(e), } diff --git a/packages/rs-sdk/src/platform/dashpay/contact_request.rs b/packages/rs-sdk/src/platform/dashpay/contact_request.rs index e9b10d77778..fd2aaa395c4 100644 --- a/packages/rs-sdk/src/platform/dashpay/contact_request.rs +++ b/packages/rs-sdk/src/platform/dashpay/contact_request.rs @@ -5,11 +5,13 @@ use crate::platform::transition::put_document::PutDocument; use crate::platform::Document; use crate::{Error, Sdk}; +use dash_platform_queries::dashpay::{ + build_contact_request_document, validate_auto_accept_proof, ContactRequestDocumentParams, +}; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{RngCore, SeedableRng}; use dpp::dashcore::secp256k1::{PublicKey, SecretKey}; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::DocumentV0; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; @@ -23,134 +25,6 @@ use platform_encryption::{ }; use std::collections::BTreeMap; -use dpp::data_contract::DataContract; - -/// 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. -#[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, - /// Optional encrypted account label: 48-80 bytes - /// (16-byte IV + 32-64 bytes of encrypted data) - pub encrypted_account_label: Option>, - /// Optional auto-accept proof (38-102 bytes) - not encrypted - pub auto_accept_proof: Option>, - /// 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], -} - -/// 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::Generic(format!( - "autoAcceptProof must be 38-102 bytes, got {}", - proof.len() - ))); - } - Ok(()) -} - -/// Build the id and property map of a DIP-15 `contactRequest` document from -/// already-derived crypto material. -/// -/// This is the pure document-assembly half of [`Sdk::create_contact_request`]: -/// the document id derives from `params.entropy`, and the property map -/// carries exactly the fields the DashPay contract defines (`toUserId`, -/// `encryptedPublicKey`, `senderKeyIndex`, `recipientKeyIndex`, -/// `accountReference`, plus the optional `encryptedAccountLabel` and -/// `autoAcceptProof`). -/// -/// Returns `(document_id, properties)`. -pub fn build_contact_request_document( - contract: &DataContract, - params: ContactRequestDocumentParams, -) -> Result<(Identifier, BTreeMap), 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::Generic(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::Generic(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::Generic("DashPay contactRequest document type not found".to_string()) - })?; - - let document_id = Document::generate_document_id_v0( - &contract.id(), - ¶ms.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_id, properties)) -} - /// ECDH provider for contact request encryption /// /// Supports two modes: @@ -387,7 +261,7 @@ impl Sdk { H: FnOnce(u32) -> Hut, Hut: std::future::Future, Error>>, { - // Validate auto accept proof size if provided. The builder + // Validate auto accept proof size if provided. The shared builder // validates again, but checking here first keeps the failure local — // before the recipient fetch and ECDH work below. if let Some(ref proof) = input.auto_accept_proof { @@ -487,14 +361,14 @@ impl Sdk { let mut xpub_iv = [0u8; 16]; rng.fill_bytes(&mut xpub_iv); - // Encrypt the extended public key (includes IV prepended). The + // Encrypt the extended public key (includes IV prepended). The shared // builder rejects any ciphertext that isn't exactly 96 bytes // (16-byte IV + 80-byte encrypted data). let encrypted_public_key = encrypt_extended_public_key(&shared_key, &xpub_iv, &extended_public_key); // Encrypt the account label if provided (includes IV prepended). The - // builder rejects any ciphertext outside 48-80 bytes + // shared builder rejects any ciphertext outside 48-80 bytes // (16-byte IV + 32-64 byte encrypted data). let encrypted_account_label = input.account_label.as_ref().map(|label| { let mut label_iv = [0u8; 16]; @@ -509,8 +383,8 @@ impl Sdk { let mut rng = StdRng::from_entropy(); let entropy = Bytes32::random_with_rng(&mut rng); - // Assemble the document in the pure builder above, keeping document - // assembly separate from this networked flow. + // Assemble the document in the shared transport-free builder, so + // networked and embedder flows produce byte-identical documents. let sender_id = input.sender_identity.id().to_owned(); let (document_id, properties) = build_contact_request_document( &dashpay_contract, diff --git a/packages/rs-sdk/src/platform/dashpay/mod.rs b/packages/rs-sdk/src/platform/dashpay/mod.rs index 1991150d643..a9e53b8778e 100644 --- a/packages/rs-sdk/src/platform/dashpay/mod.rs +++ b/packages/rs-sdk/src/platform/dashpay/mod.rs @@ -7,13 +7,14 @@ mod contact_request; mod contact_request_queries; pub use contact_request::{ - build_contact_request_document, recipient_key_purpose_is_acceptable_on_receive, - recipient_key_purpose_is_valid, sender_key_purpose_is_acceptable_on_receive, - validate_auto_accept_proof, ContactRequestDocumentParams, ContactRequestInput, - ContactRequestResult, EcdhProvider, RecipientIdentity, SendContactRequestInput, - SendContactRequestResult, + recipient_key_purpose_is_acceptable_on_receive, recipient_key_purpose_is_valid, + sender_key_purpose_is_acceptable_on_receive, ContactRequestInput, ContactRequestResult, + EcdhProvider, RecipientIdentity, SendContactRequestInput, SendContactRequestResult, }; pub use contact_request_queries::ContactRequestDocuments; +pub use dash_platform_queries::dashpay::{ + build_contact_request_document, validate_auto_accept_proof, ContactRequestDocumentParams, +}; use crate::platform::Fetch; use crate::{Error, Sdk}; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index eacec643775..15021d9042d 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -3,7 +3,8 @@ mod queries; pub use contested_queries::ContestedDpnsUsername; pub use dash_platform_queries::dpns_usernames::{ - convert_to_homograph_safe_chars, is_contested_username, is_valid_username, + build_dpns_preorder_and_domain_documents, convert_to_homograph_safe_chars, + is_contested_username, is_valid_username, }; pub use queries::DpnsUsername; @@ -11,19 +12,15 @@ use crate::platform::transition::put_document::PutDocument; use crate::platform::{Document, Fetch, FetchMany}; use crate::{Error, Sdk}; use dash_context_provider::ContextProvider; -use dash_platform_queries::dpns_usernames::is_consensus_valid_label; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::DataContract; -use dpp::document::{DocumentV0, DocumentV0Getters}; +use dpp::document::DocumentV0Getters; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::signer::Signer; use dpp::identity::{Identity, IdentityPublicKey}; use dpp::platform_value::{Bytes32, Value}; use dpp::prelude::Identifier; -use std::collections::BTreeMap; use std::sync::Arc; fn extract_dpns_label(name: &str) -> &str { @@ -47,165 +44,6 @@ fn normalize_dpns_label(input: &str) -> String { convert_to_homograph_safe_chars(extract_dpns_label(input)) } -/// Hash a buffer twice using SHA256 (double SHA256) -fn hash_double(data: Vec) -> [u8; 32] { - use dpp::dashcore::hashes::{sha256d, Hash}; - // sha256d already does double SHA256 - let hash = sha256d::Hash::hash(&data); - hash.to_byte_array() -} - -/// Build the DPNS `preorder` and `domain` documents that register -/// `label`.dash for `identity_id`, exactly as platform consensus expects -/// them. -/// -/// This is the pure document-assembly half of [`Sdk::register_dpns_name`]: -/// no networking, and no randomness — the caller supplies the `entropy` -/// that derives both document ids (the same entropy must later be attached -/// to both create transitions) and the preorder `salt`, whose double-SHA256 -/// over `salt ‖ ".dash"` becomes the preorder's -/// `saltedDomainHash`. -/// -/// The `label` must satisfy [`is_consensus_valid_label`]; the raw label is stored -/// in the domain document's `label` property while its -/// [homograph-safe](convert_to_homograph_safe_chars) form is stored in -/// `normalizedLabel`. -/// -/// # Salt secrecy and reveal order -/// -/// The preorder/domain split is DPNS's front-running protection: the -/// preorder commits to `saltedDomainHash` without revealing which name -/// is being registered, and only the later domain document discloses -/// the `label` and the `preorderSalt` that tie it to the commitment. -/// That protection holds only if the caller upholds what -/// [`Sdk::register_dpns_name`] does automatically: -/// -/// - generate a **fresh 32-byte salt from a CSPRNG** for every -/// registration attempt (the SDK draws it from -/// `StdRng::from_entropy()`). A reused or predictable salt lets an -/// observer precompute `sha256d(salt ‖ ".dash")` for -/// candidate labels and identify — then front-run — the name from -/// the preorder alone; -/// - keep the salt, the label, and the assembled domain document -/// **private until the preorder create transition is confirmed** -/// (the SDK submits the preorder and waits for its response before -/// broadcasting the domain document). Revealing them earlier -/// discloses the name while it is still unclaimed, defeating the -/// commitment. -/// -/// Callers driving their own flow inherit both obligations — this -/// builder takes `salt` as an argument precisely because it has no -/// randomness of its own and cannot enforce either one. -/// -/// Returns `(preorder_document, domain_document)`. -pub fn build_dpns_preorder_and_domain_documents( - contract: &DataContract, - identity_id: Identifier, - label: &str, - entropy: [u8; 32], - salt: [u8; 32], -) -> Result<(Document, Document), Error> { - if !is_consensus_valid_label(label) { - return Err(Error::Generic(format!( - "Invalid DPNS label \"{label}\": must be 3-63 characters, alphanumeric and hyphens \ - only, starting and ending with an alphanumeric character" - ))); - } - - let preorder_document_type = contract - .document_type_for_name("preorder") - .map_err(|_| Error::Generic("DPNS preorder document type not found".to_string()))?; - - let domain_document_type = contract - .document_type_for_name("domain") - .map_err(|_| Error::Generic("DPNS domain document type not found".to_string()))?; - - let preorder_id = Document::generate_document_id_v0( - &contract.id(), - &identity_id, - preorder_document_type.name(), - entropy.as_slice(), - ); - let domain_id = Document::generate_document_id_v0( - &contract.id(), - &identity_id, - domain_document_type.name(), - entropy.as_slice(), - ); - - // Create salted domain hash for preorder - let normalized_label = convert_to_homograph_safe_chars(label); - let mut salted_domain_buffer: Vec = vec![]; - salted_domain_buffer.extend(salt); - salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); - let salted_domain_hash = hash_double(salted_domain_buffer); - - let preorder_document = Document::V0(DocumentV0 { - id: preorder_id, - owner_id: identity_id, - properties: BTreeMap::from([( - "saltedDomainHash".to_string(), - Value::Bytes32(salted_domain_hash), - )]), - 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, - }); - - let domain_document = Document::V0(DocumentV0 { - id: domain_id, - owner_id: identity_id, - properties: BTreeMap::from([ - ( - "parentDomainName".to_string(), - Value::Text("dash".to_string()), - ), - ( - "normalizedParentDomainName".to_string(), - Value::Text("dash".to_string()), - ), - ("label".to_string(), Value::Text(label.to_string())), - ("normalizedLabel".to_string(), Value::Text(normalized_label)), - ("preorderSalt".to_string(), Value::Bytes32(salt)), - ( - "records".to_string(), - Value::Map(vec![( - Value::Text("identity".to_string()), - Value::Identifier(identity_id.to_buffer()), - )]), - ), - ( - "subdomainRules".to_string(), - Value::Map(vec![( - Value::Text("allowSubdomains".to_string()), - Value::Bool(false), - )]), - ), - ]), - 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, - }); - - Ok((preorder_document, domain_document)) -} - /// Callback type for preorder document pub type PreorderCallback = Box; @@ -317,8 +155,8 @@ impl Sdk { let entropy = Bytes32::random_with_rng(&mut rng); let salt: [u8; 32] = rng.gen(); - // Assemble both documents in the pure builder above, keeping - // document assembly separate from this networked flow. + // Assemble both documents in the shared transport-free builder, so + // networked and embedder flows produce byte-identical documents. let (preorder_document, domain_document) = build_dpns_preorder_and_domain_documents( &dpns_contract, input.identity.id().to_owned(), From f778c9b05e00c75a365930ae18225955e17c4131 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 22 Aug 2026 12:28:31 -0500 Subject: [PATCH 5/8] refactor(sdk): decode document queries from the wire request in shared client code DocumentQuery::try_from_request decodes a wire GetDocumentsRequest back into a rich DocumentQuery - the inverse of request encoding. V1 typed clauses go through the same proto_conversions functions the server's v1 handler runs; V0 CBOR where/order_by fields are decoded exactly as the server's query_documents_v0 does. Multi-projection selects and limit Some(0) are rejected, mirroring the server's contracts. --- Cargo.lock | 1 + packages/dash-platform-queries/Cargo.toml | 1 + .../src/documents/document_query.rs | 267 ++++++++++++++++++ .../src/documents/mod.rs | 7 +- packages/dash-platform-queries/src/error.rs | 16 ++ 5 files changed, 289 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92026cb5b5d..f0a20a6cd53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,6 +1706,7 @@ dependencies = [ name = "dash-platform-queries" version = "4.2.0-dev.1" dependencies = [ + "ciborium", "dapi-grpc", "dash-context-provider", "dash-platform-macros", diff --git a/packages/dash-platform-queries/Cargo.toml b/packages/dash-platform-queries/Cargo.toml index f3d76e7c747..7c9870a7392 100644 --- a/packages/dash-platform-queries/Cargo.toml +++ b/packages/dash-platform-queries/Cargo.toml @@ -17,6 +17,7 @@ mocks = [ ] [dependencies] +ciborium = { version = "0.2.2" } dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ "platform", "client", diff --git a/packages/dash-platform-queries/src/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs index 113e6e1e478..856fa4fa1ea 100644 --- a/packages/dash-platform-queries/src/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -2,6 +2,7 @@ use std::sync::Arc; +use super::proto_conversions; use crate::error::Error; use dapi_grpc::platform::v0::get_documents_request::Version::{V0, V1}; use dapi_grpc::platform::v0::{ @@ -383,6 +384,272 @@ impl DocumentQuery { ) -> Result { GetDocumentsRequest::try_from_platform_versioned(self, platform_version) } + + /// Decode a wire-format [`GetDocumentsRequest`] back into a rich + /// [`DocumentQuery`] — the inverse of + /// [`Self::try_into_request_for_version`], and the piece that lets + /// a client recover the query given only the request bytes it + /// sent. + /// + /// Both wire versions are handled, mirroring how the server + /// decodes each: + /// - **V0** carries `where` / `order_by` as CBOR-encoded arrays of + /// clause components; they are decoded exactly as + /// rs-drive-abci's `query_documents_v0` does (ciborium → + /// `Value::Array` → `WhereClause::from_components` / + /// `OrderClause::from_components`). V0 has no `select` / + /// `group_by` / `having` / `offset`; those default to the + /// documents-fetch shape. + /// - **V1** carries typed proto clauses; they are decoded through + /// the same [`proto_conversions`](super::proto_conversions) + /// functions the server's v1 handler runs, so client and server + /// cannot disagree on what the bytes mean. Multi-projection + /// `selects` (len > 1) is rejected — a `DocumentQuery` carries a + /// single projection, matching what the server evaluates. + /// `limit: Some(0)` is rejected, mirroring the server's uniform + /// `InvalidLimit` contract (`None` = server default → `0` + /// sentinel here; only positive caps are representable). + /// + /// The `prove` flag is intentionally ignored: `DocumentQuery` has + /// no prove field (its encoders always set `prove: true`, because + /// the `FromProof` decoders only handle proved responses). + /// + /// `contract` must be the data contract the request targets — the + /// request's `data_contract_id` is checked against `contract.id()` + /// and the named document type must exist on it. + /// + /// Scope caveat: this mirrors the server's *wire-shape* decoding + /// (shared clause decoders), not its full `validate_and_route` + /// business rules — e.g. SUM/AVG requiring a non-empty field, + /// GROUP BY being illegal with SELECT DOCUMENTS, or HAVING being + /// unimplemented are enforced server-side only. A request violating + /// those decodes here but can never yield a provable response from + /// a real server. That gap matters precisely for fabricated + /// request/response pairs, so a proof-verifying entry point built + /// on this decode must reject every such shape before delegating, + /// rather than letting the lowering to [`DriveDocumentQuery`] + /// silently drop it. + pub fn try_from_request( + request: GetDocumentsRequest, + contract: Arc, + ) -> Result { + match request.version { + Some(V0(request_v0)) => Self::try_from_request_v0(request_v0, contract), + Some(V1(request_v1)) => Self::try_from_request_v1(request_v1, contract), + None => Err(Error::Protocol(ProtocolError::DecodingError( + "GetDocumentsRequest has no version set".to_string(), + ))), + } + } + + fn try_from_request_v0( + request: GetDocumentsRequestV0, + contract: Arc, + ) -> Result { + let GetDocumentsRequestV0 { + data_contract_id, + document_type, + r#where, + order_by, + limit, + // See `try_from_request`: DocumentQuery has no prove field. + prove: _, + start, + } = request; + + check_request_targets_contract(&contract, &data_contract_id, &document_type)?; + + let where_clauses = where_clauses_from_cbor(&r#where)?; + let order_by_clauses = order_clauses_from_cbor(&order_by)?; + + Ok(Self { + select: SelectProjection::documents(), + data_contract: contract, + document_type_name: document_type, + where_clauses, + group_by: Vec::new(), + having: Vec::new(), + order_by_clauses, + // V0's plain `uint32` uses the same `0` = "unset" sentinel + // as this struct — pass through. + limit, + offset: None, + start, + }) + } + + fn try_from_request_v1( + request: GetDocumentsRequestV1, + contract: Arc, + ) -> Result { + let GetDocumentsRequestV1 { + data_contract_id, + document_type, + where_clauses, + order_by, + limit, + start, + // See `try_from_request`: DocumentQuery has no prove field. + prove: _, + selects, + group_by, + having, + offset, + } = request; + + check_request_targets_contract(&contract, &data_contract_id, &document_type)?; + + let where_clauses = proto_conversions::where_clauses_from_proto(where_clauses)?; + let order_by_clauses = proto_conversions::order_clauses_from_proto(order_by)?; + let having = proto_conversions::having_clauses_from_proto(having)?; + + // Same shape the server's v1 handler accepts: 0 selects → + // default documents projection, 1 select → decode it, more → + // reject (a `DocumentQuery` carries a single projection; + // multi-projection is wire-only today and the server refuses + // it too). + if selects.len() > 1 { + return Err(Error::Protocol(ProtocolError::DecodingError(format!( + "multi-projection SELECT is not supported: a DocumentQuery carries a \ + single projection, got {} selects", + selects.len() + )))); + } + let select = selects + .into_iter() + .next() + .map(proto_conversions::select_from_proto) + .transpose()? + .unwrap_or_else(SelectProjection::documents); + + // Mirror the server's uniform v1 limit contract: `None` = use + // the server default (the `0` sentinel here), positive = + // explicit cap, `Some(0)` invalid (and unrepresentable — this + // struct's `0` means "unset"). + let limit = match limit { + None => 0, + Some(0) => { + return Err(Error::Protocol(ProtocolError::DecodingError( + "limit = 0 is not a valid wire value on the v1 `optional uint32` \ + field; omit `limit` (None) to use the server's default, or pass \ + a positive integer for an explicit cap" + .to_string(), + ))); + } + Some(n) => n, + }; + + // V1 ships its own `Start` enum with the same shape as V0's; + // this struct stores the V0 type (see `encode_v1` for the + // inverse translation). + let start = start.map(|s| match s { + V1Start::StartAfter(b) => Start::StartAfter(b), + V1Start::StartAt(b) => Start::StartAt(b), + }); + + Ok(Self { + select, + data_contract: contract, + document_type_name: document_type, + where_clauses, + group_by, + having, + order_by_clauses, + limit, + offset, + start, + }) + } +} + +/// Shared request-vs-contract consistency check for both wire +/// versions: the request must target the supplied contract, and the +/// named document type must exist on it. +fn check_request_targets_contract( + contract: &DataContract, + data_contract_id: &[u8], + document_type_name: &str, +) -> Result<(), Error> { + if data_contract_id != contract.id().as_slice() { + return Err(Error::Protocol(ProtocolError::DecodingError(format!( + "GetDocumentsRequest targets data contract {} but the supplied contract is {}", + hex::encode(data_contract_id), + contract.id() + )))); + } + contract + .document_type_for_name(document_type_name) + .map_err(ProtocolError::DataContractError)?; + Ok(()) +} + +/// Decode a V0 `where` field — CBOR bytes carrying an array of +/// `[field, operator, value]` component arrays — into structured +/// clauses. Byte-for-byte mirror of the decode the server's +/// `query_documents_v0` runs (empty bytes → no clauses; anything +/// else must be a CBOR array of arrays). +fn where_clauses_from_cbor(bytes: &[u8]) -> Result, Error> { + if bytes.is_empty() { + return Ok(Vec::new()); + } + let value: Value = ciborium::de::from_reader(bytes).map_err(|_| { + Error::Protocol(ProtocolError::DecodingError( + "unable to decode 'where' query from cbor".to_string(), + )) + })?; + match value { + Value::Null => Ok(Vec::new()), + Value::Array(clauses) => clauses + .iter() + .map(|wc| match wc { + Value::Array(components) => { + WhereClause::from_components(components).map_err(Error::Drive) + } + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "where clause must be an array".to_string(), + ))), + }) + .collect(), + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "where clause must be an array".to_string(), + ))), + } +} + +/// Decode a V0 `order_by` field — CBOR bytes carrying an array of +/// `[field, "asc"|"desc"]` component arrays — into structured +/// clauses. Mirror of the server-side decode, like +/// [`where_clauses_from_cbor`]. +fn order_clauses_from_cbor(bytes: &[u8]) -> Result, Error> { + if bytes.is_empty() { + return Ok(Vec::new()); + } + let value: Value = ciborium::de::from_reader(bytes).map_err(|_| { + Error::Protocol(ProtocolError::DecodingError( + "unable to decode 'order_by' query from cbor".to_string(), + )) + })?; + match value { + Value::Null => Ok(Vec::new()), + Value::Array(clauses) => clauses + .iter() + .map(|oc| match oc { + Value::Array(components) => { + OrderClause::from_components(components).map_err(|_| { + Error::Protocol(ProtocolError::DecodingError( + "invalid order_by clause components".to_string(), + )) + }) + } + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "order_by clause must be an array".to_string(), + ))), + }) + .collect(), + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "order_by must be an array".to_string(), + ))), + } } impl FromProof for Document { diff --git a/packages/dash-platform-queries/src/documents/mod.rs b/packages/dash-platform-queries/src/documents/mod.rs index 6c3d07d6b15..eb9d7938173 100644 --- a/packages/dash-platform-queries/src/documents/mod.rs +++ b/packages/dash-platform-queries/src/documents/mod.rs @@ -12,9 +12,10 @@ pub mod document_split_sums; pub mod document_sum; pub(crate) mod having_proof_helpers; /// Client-side wire-proto → drive-type decoders for `getDocuments`, -/// mirroring rs-drive-abci's server request decode; the two must be -/// kept in lockstep (see the module docs). -#[allow(dead_code)] // consumer (`DocumentQuery::try_from_request`) lands next +/// consumed by +/// [`document_query::DocumentQuery::try_from_request`]. They mirror +/// rs-drive-abci's server request decode and must be kept in +/// lockstep with it (see the module docs). pub(crate) mod proto_conversions; pub(crate) mod ranked_proof_helpers; pub(crate) mod sum_proof_helpers; diff --git a/packages/dash-platform-queries/src/error.rs b/packages/dash-platform-queries/src/error.rs index ee42f67b7e2..f30573c3a72 100644 --- a/packages/dash-platform-queries/src/error.rs +++ b/packages/dash-platform-queries/src/error.rs @@ -29,6 +29,22 @@ pub enum Error { Protocol(#[from] ProtocolError), } +impl From for Error { + fn from(value: crate::documents::proto_conversions::DecodeError) -> Self { + use crate::documents::proto_conversions::DecodeError; + match value { + // Malformed wire bytes — a decoding failure, not a + // misconfiguration. + DecodeError::InvalidArgument(msg) => Self::Protocol(ProtocolError::DecodingError(msg)), + // Well-formed wire shape the decode target can't express + // yet — same classification the server gives it. + DecodeError::Unsupported(msg) => Self::Drive(drive::error::Error::Query( + drive::error::query::QuerySyntaxError::Unsupported(msg), + )), + } + } +} + impl From for Error { fn from(value: ConsensusError) -> Self { Self::Protocol(ProtocolError::ConsensusError(Box::new(value))) From 29c4233bd983ea7d1ad4e9beae05a45ed5c94106 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 22 Aug 2026 12:30:47 -0500 Subject: [PATCH 6/8] feat(sdk): request-bound document proof verification shared by SDK and embedders verify_documents_response verifies a proved GetDocumentsResponse directly against the wire request that produced it: the wire version (V0/V1 oneof arm) is checked against the platform version's document_query feature bounds (the server's own dispatch gate), prove=false requests are rejected (an honest server answers them unproved), and the request decodes through the shared try_from_request before delegating to FromProof. The query-shape gates (HAVING, GROUP BY, OFFSET, non-documents SELECT - every field the DocumentQuery -> DriveDocumentQuery lowering drops) run inside the shared FromProof impl itself rather than only at the wire entry point. dash-sdk's document fetches verify through that impl, and the SDK talks to the same untrusted evonodes an embedder's transport does, so both paths now reject request shapes no honest server would have proved before any proof machinery runs. --- packages/dash-platform-queries/README.md | 21 +- .../src/documents/document_query.rs | 262 +++++++++++++++++- 2 files changed, 267 insertions(+), 16 deletions(-) diff --git a/packages/dash-platform-queries/README.md b/packages/dash-platform-queries/README.md index 1587d9a58df..b4b71d1bab8 100644 --- a/packages/dash-platform-queries/README.md +++ b/packages/dash-platform-queries/README.md @@ -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 diff --git a/packages/dash-platform-queries/src/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs index 856fa4fa1ea..5b9699a8dc3 100644 --- a/packages/dash-platform-queries/src/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -388,8 +388,8 @@ impl DocumentQuery { /// Decode a wire-format [`GetDocumentsRequest`] back into a rich /// [`DocumentQuery`] — the inverse of /// [`Self::try_into_request_for_version`], and the piece that lets - /// a client recover the query given only the request bytes it - /// sent. + /// an embedder verify a proved response given only the request + /// bytes it sent (see [`verify_documents_response`]). /// /// Both wire versions are handled, mirroring how the server /// decodes each: @@ -425,10 +425,10 @@ impl DocumentQuery { /// unimplemented are enforced server-side only. A request violating /// those decodes here but can never yield a provable response from /// a real server. That gap matters precisely for fabricated - /// request/response pairs, so a proof-verifying entry point built - /// on this decode must reject every such shape before delegating, - /// rather than letting the lowering to [`DriveDocumentQuery`] - /// silently drop it. + /// request/response pairs, so the proof-verifying entry point + /// [`verify_documents_response`] closes it: it rejects every such + /// shape before delegating, rather than letting the lowering to + /// [`DriveDocumentQuery`] silently drop it. pub fn try_from_request( request: GetDocumentsRequest, contract: Arc, @@ -652,6 +652,250 @@ fn order_clauses_from_cbor(bytes: &[u8]) -> Result, Error> { } } +/// Reject a request whose wire version (the `V0`/`V1` oneof arm, +/// i.e. feature version 0/1) falls outside the supplied platform +/// version's `drive_abci.query.document_query` bounds. +/// +/// This is the same `check_version` gate the server runs before it +/// decodes anything (`Platform::query_documents` in rs-drive-abci, +/// which answers an out-of-bounds wire version with +/// `QueryError::UnsupportedQueryVersion`). Without it, an untrusted +/// transport could pair a request wire-version the supplied platform +/// version's server refuses to serve with a valid proof produced for +/// the other wire shape, and verification would accept the pair. +/// +/// A missing `version` oneof is deliberately let through — the decode +/// that follows reports it with its established error message. +fn check_wire_version_is_served( + request: &GetDocumentsRequest, + platform_version: &PlatformVersion, +) -> Result<(), drive_proof_verifier::Error> { + let Some(version) = &request.version else { + return Ok(()); + }; + let feature_version: u16 = match version { + V0(_) => 0, + V1(_) => 1, + }; + let bounds = &platform_version.drive_abci.query.document_query; + if !bounds.check_version(feature_version) { + return Err(drive_proof_verifier::Error::RequestError { + error: format!( + "GetDocumentsRequest wire version V{feature_version} is outside the \ + document_query feature-version bounds {}..={} served at platform version \ + {}; the server answers such a request with UnsupportedQueryVersion, so no \ + proved response can belong to it", + bounds.min_version, bounds.max_version, platform_version.protocol_version + ), + }); + } + Ok(()) +} + +/// Reject the request shapes that can never have produced the proved +/// plain-document response being verified. +/// +/// Each rejection mirrors a gate an honest server runs before it would +/// ever build such a proof, and each covers a field the +/// `DocumentQuery` → [`DriveDocumentQuery`] lowering discards — which +/// is exactly the set an attacker could vary freely while replaying a +/// genuine proof. See [`verify_documents_response`] for the threat +/// model. +/// +/// This runs inside the [`FromProof`] impl itself — the +/// shared choke point — so every `DocumentQuery`-keyed verification +/// gets it: `dash-sdk`'s own document fetches talk to the same +/// untrusted nodes an embedder's transport does, and are protected by +/// the same gates as the wire-request entry point +/// [`verify_documents_response`]. +/// +/// Server counterparts, all in +/// `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`: +/// `validate_and_route` rejects a non-empty HAVING for any +/// non-aggregate SELECT and a non-empty GROUP BY under SELECT +/// DOCUMENTS; `reject_offset_off_the_ranked_path` rejects any OFFSET +/// that did not route to the ranked executor (a documents fetch never +/// does). +fn reject_request_the_server_would_not_have_proved( + query: &DocumentQuery, +) -> Result<(), drive_proof_verifier::Error> { + let reject = |error: String| Err(drive_proof_verifier::Error::RequestError { error }); + + // This path verifies plain document fetches only. An aggregate + // projection (COUNT/SUM/AVG) is proved with a different proof shape; + // handing it to the Documents verifier would surface as an opaque + // low-level proof error, so reject it up front instead. + if query.select != drive::query::SelectProjection::documents() { + return reject(format!( + "only a plain SELECT DOCUMENTS fetch can be verified here; the request carries \ + the projection {:?} — aggregate projections are verified by the aggregate \ + proof helpers", + query.select + )); + } + if !query.having.is_empty() { + return reject(format!( + "request carries {} HAVING clause(s), which the server refuses for a \ + non-aggregate SELECT; no proved document response can belong to it", + query.having.len() + )); + } + if !query.group_by.is_empty() { + return reject(format!( + "request carries GROUP BY {:?}, which the server refuses under SELECT DOCUMENTS; \ + no proved document response can belong to it", + query.group_by + )); + } + if let Some(offset) = query.offset { + return reject(format!( + "request carries OFFSET {offset}, which the server accepts only on the ranked \ + surface, never for a document fetch; no proved document response can belong to it" + )); + } + Ok(()) +} + +/// Embedder entry point: verify a proved [`GetDocumentsResponse`] +/// directly against the wire request that produced it. +/// +/// This is the transport-free glue an embedder needs when it drives +/// its own transport: it holds the `GetDocumentsRequest` it sent and +/// the `GetDocumentsResponse` it got back, and this function does the +/// rest — decodes the request into a [`DocumentQuery`] (via +/// [`DocumentQuery::try_from_request`], whose decoders mirror the +/// server's request decode) and delegates to the existing +/// [`FromProof`] machinery, which resolves the +/// [`DriveDocumentQuery`] internally and cryptographically verifies +/// the proof against it. +/// +/// `contract` must be the data contract the request targets. If the +/// embedder's [`ContextProvider`] can resolve contracts, use +/// [`verify_documents_response_with_provider_contract`] instead and +/// skip the explicit parameter. +/// +/// # Binding the proof to the whole request +/// +/// GroveDB and Tenderdash proofs authenticate the state and the +/// resolved [`DriveDocumentQuery`] — not the request envelope. The +/// rich→drive lowering drops request fields that a documents query has +/// no place for (`group_by`, `having`, `offset`, `prove`), so +/// delegating without first checking them would let an untrusted +/// transport pair a request the real server would have *refused* with +/// a valid proof for the narrower query it lowers to, and this +/// function would accept it. Every such field is therefore rejected +/// before any proof machinery runs — `prove` here on the wire request, +/// and the query-shape fields inside the shared +/// [`FromProof`] impl (so `dash-sdk`'s own fetches run +/// the identical gates) — mirroring the server's own gates in +/// `rs-drive-abci`'s `validate_and_route` / +/// `reject_offset_off_the_ranked_path`. +/// +/// The same reasoning covers the request envelope itself: the wire +/// version (`V0`/`V1` oneof arm) is checked against +/// `platform_version.drive_abci.query.document_query`'s bounds before +/// anything is decoded — the server's `query_documents` dispatch +/// refuses an out-of-bounds wire version with +/// `UnsupportedQueryVersion`, so a proof can never belong to one — and +/// the query limit contract is enforced during the +/// `DriveDocumentQuery` lowering exactly as +/// `DriveDocumentQuery::from_typed_clauses` enforces it server-side: +/// an omitted limit resolves to the server default and a limit above +/// [`DEFAULT_QUERY_LIMIT`] is rejected. +pub fn verify_documents_response( + request: GetDocumentsRequest, + contract: Arc, + response: platform_proto::GetDocumentsResponse, + network: Network, + platform_version: &PlatformVersion, + provider: &dyn ContextProvider, +) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> { + // First gate, mirroring the server's own dispatch order: a wire + // version the supplied platform version's server refuses to serve + // is rejected before any decoding or proof machinery. + check_wire_version_is_served(&request, platform_version)?; + // `prove` does not survive decoding (a `DocumentQuery` has no such + // field), so read it off the wire request before it is consumed. + // `prove: false` is not a server rejection — it makes the server + // return an unproved response, so a proved response cannot have + // come from one. + let prove = match &request.version { + Some(V0(v0)) => v0.prove, + Some(V1(v1)) => v1.prove, + // Missing version is reported by the decode below. + None => true, + }; + if !prove { + return Err(drive_proof_verifier::Error::RequestError { + error: "request carries prove=false, so an honest server would have answered it \ + with an unproved response; a proved response cannot belong to this request" + .to_string(), + }); + } + let query = DocumentQuery::try_from_request(request, contract).map_err(|e| { + drive_proof_verifier::Error::RequestError { + error: format!("failed to decode GetDocumentsRequest into a DocumentQuery: {e}"), + } + })?; + // The remaining request-shape gates + // (`reject_request_the_server_would_not_have_proved`) run inside the + // shared `FromProof` impl this delegates to. + >::maybe_from_proof_with_metadata( + query, + response, + network, + platform_version, + provider, + ) +} + +/// Variant of [`verify_documents_response`] that resolves the data +/// contract through the [`ContextProvider`] +/// ([`ContextProvider::get_data_contract`]) instead of taking it as a +/// parameter — for embedders whose provider already caches or fetches +/// contracts. +pub fn verify_documents_response_with_provider_contract( + request: GetDocumentsRequest, + response: platform_proto::GetDocumentsResponse, + network: Network, + platform_version: &PlatformVersion, + provider: &dyn ContextProvider, +) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> { + // Same first gate as `verify_documents_response`, run here as well + // so an out-of-bounds wire version is rejected before the provider + // is asked for anything (the contract lookup below is already + // context-provider machinery). + check_wire_version_is_served(&request, platform_version)?; + let contract_id_bytes = match &request.version { + Some(V0(v0)) => v0.data_contract_id.as_slice(), + Some(V1(v1)) => v1.data_contract_id.as_slice(), + None => { + return Err(drive_proof_verifier::Error::RequestError { + error: "GetDocumentsRequest has no version set".to_string(), + }); + } + }; + let contract_id = Identifier::from_bytes(contract_id_bytes).map_err(|e| { + drive_proof_verifier::Error::RequestError { + error: format!("invalid data_contract_id in GetDocumentsRequest: {e}"), + } + })?; + let contract = provider + .get_data_contract(&contract_id, platform_version) + .map_err(drive_proof_verifier::Error::ContextProviderError)? + .ok_or_else(|| drive_proof_verifier::Error::RequestError { + error: format!("context provider has no data contract {contract_id}"), + })?; + verify_documents_response( + request, + contract, + response, + network, + platform_version, + provider, + ) +} + impl FromProof for Document { type Request = DocumentQuery; type Response = platform_proto::GetDocumentsResponse; @@ -706,6 +950,12 @@ impl FromProof for drive_proof_verifier::types::Documents { Self: Sized + 'a, { let request: Self::Request = request.into(); + // Server-parity request gates run here, at the shared choke + // point, so every `DocumentQuery`-keyed verification — dash-sdk + // fetches and the wire-request entry points alike — rejects + // request shapes no honest server would have proved before any + // proof machinery runs. + reject_request_the_server_would_not_have_proved(&request)?; let drive_query: DriveDocumentQuery = (&request) .try_into() From 1aa80814b17f226b57a1e9c7154cfa989ba50034 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 20 Aug 2026 09:21:41 -0500 Subject: [PATCH 7/8] refactor(sdk): share document transition preparation with embedders Extracts the document create/replace preparation out of dash-sdk's PutDocument broadcast path into dash-platform-queries: property sanitization for the transition (prepare_document_for_transition) and the entropy/document-id consistency check (ensure_entropy_matches_document_id) that surfaces an id/entropy drift locally instead of after the broadcast has paid a bumped identity-contract nonce. dash-sdk delegates to the shared helpers with unchanged behavior; transport-free embedders (packages/rs-platform-cxx) assemble their own transitions through the same code instead of reimplementing it in C++. Split out of #4389 to keep that PR to its declared decode/builders/verification scope. --- .../src/transition/mod.rs | 1 + .../src/transition/put_document.rs | 176 ++++++++++++++++++ .../src/platform/transition/put_document.rs | 166 +---------------- 3 files changed, 182 insertions(+), 161 deletions(-) create mode 100644 packages/dash-platform-queries/src/transition/put_document.rs diff --git a/packages/dash-platform-queries/src/transition/mod.rs b/packages/dash-platform-queries/src/transition/mod.rs index 3a0f1376adb..097952677ce 100644 --- a/packages/dash-platform-queries/src/transition/mod.rs +++ b/packages/dash-platform-queries/src/transition/mod.rs @@ -1,2 +1,3 @@ //! Transport-free state transition helpers. +pub mod put_document; pub mod validation; diff --git a/packages/dash-platform-queries/src/transition/put_document.rs b/packages/dash-platform-queries/src/transition/put_document.rs new file mode 100644 index 00000000000..3449bb5e672 --- /dev/null +++ b/packages/dash-platform-queries/src/transition/put_document.rs @@ -0,0 +1,176 @@ +//! Transport-free helpers for document create/replace transitions. +//! +//! `dash-sdk`'s `PutDocument` broadcast path calls these; embedders that +//! assemble their own transitions share the same preparation and +//! entropy/id consistency check. + +use crate::Error; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::DocumentType; +use dpp::document::{Document, DocumentV0Getters}; +use dpp::prelude::Identifier; + +/// Returns a copy of `document` with its properties sanitized for the given +/// document type (e.g. integer arrays coerced back into byte arrays after a +/// WASM boundary crossing), leaving the caller's document untouched. +pub fn prepare_document_for_transition( + document: &Document, + document_type: &DocumentType, +) -> Document { + let mut document = document.clone(); + document_type + .as_ref() + .sanitize_document_properties(document.properties_mut()); + document +} + +/// Ensures a caller-supplied `entropy` derives the same document id already set +/// on a create document. +/// +/// A document-create state transition carries both the document id and the +/// entropy, and Drive recomputes the id from the entropy during +/// `advanced_structure` validation, rejecting the transition with +/// `InvalidDocumentTransitionIdError` when they disagree. Because the +/// broadcast path trusts the caller's id verbatim when entropy is supplied, +/// a two-phase caller whose id and entropy have drifted would only discover +/// the mismatch after paying (a bumped identity-contract nonce). This check +/// surfaces the mismatch locally before broadcasting. +pub fn ensure_entropy_matches_document_id( + contract_id: &Identifier, + owner_id: &Identifier, + document_type_name: &str, + entropy: &[u8; 32], + document_id: Identifier, +) -> Result<(), Error> { + let expected_id = Document::generate_document_id_v0( + contract_id, + owner_id, + document_type_name, + entropy.as_slice(), + ); + if expected_id != document_id { + return Err(Error::InvalidInput(format!( + "document id {document_id} does not match the id {expected_id} derived from the \ + supplied entropy; the entropy must be the one used to generate the document id" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::data_contract::config::DataContractConfig; + use dpp::document::{DocumentV0, INITIAL_REVISION}; + use dpp::platform_value::{platform_value, Value}; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + fn contract_id() -> Identifier { + Identifier::from([1u8; 32]) + } + + fn owner_id() -> Identifier { + Identifier::from([2u8; 32]) + } + + #[test] + fn matching_entropy_and_id_pass() { + let entropy = [7u8; 32]; + let id = Document::generate_document_id_v0( + &contract_id(), + &owner_id(), + "contactRequest", + entropy.as_slice(), + ); + + ensure_entropy_matches_document_id( + &contract_id(), + &owner_id(), + "contactRequest", + &entropy, + id, + ) + .expect("id derived from the supplied entropy must be accepted"); + } + + #[test] + fn mismatched_entropy_and_id_error_before_broadcast() { + // The id was derived from E1, but the caller passes E2 != E1 (mirroring + // the very drift consensus rejects with InvalidDocumentTransitionIdError). + let entropy_used = [1u8; 32]; + let id = Document::generate_document_id_v0( + &contract_id(), + &owner_id(), + "contactRequest", + entropy_used.as_slice(), + ); + + let different_entropy = [2u8; 32]; + let result = ensure_entropy_matches_document_id( + &contract_id(), + &owner_id(), + "contactRequest", + &different_entropy, + id, + ); + + assert!( + matches!(result, Err(Error::InvalidInput(_))), + "a document id derived from a different entropy must be rejected locally" + ); + } + + #[test] + fn should_normalize_wasm_uint8_array_property_without_mutating_caller_document() { + let platform_version = PlatformVersion::latest(); + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create default data contract config"); + let document_type = DocumentType::try_from_schema( + contract_id(), + 1, + config.version(), + "preorder", + platform_value!({ + "type": "object", + "properties": { + "saltedDomainHash": { + "type": "array", + "byteArray": true, + "minItems": 32_u32, + "maxItems": 32_u32, + "position": 0 + } + }, + "required": ["saltedDomainHash"], + "additionalProperties": false, + }), + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("should create DPNS-like document type"); + let integer_array = Value::Array(vec![Value::U64(7); 32]); + let document = Document::V0(DocumentV0 { + id: Identifier::new([3; 32]), + owner_id: owner_id(), + properties: BTreeMap::from([("saltedDomainHash".to_string(), integer_array.clone())]), + revision: Some(INITIAL_REVISION), + ..Default::default() + }); + + let prepared = prepare_document_for_transition(&document, &document_type); + + assert_eq!( + prepared.properties().get("saltedDomainHash"), + Some(&Value::Bytes32([7; 32])) + ); + assert_eq!( + document.properties().get("saltedDomainHash"), + Some(&integer_array) + ); + } +} diff --git a/packages/rs-sdk/src/platform/transition/put_document.rs b/packages/rs-sdk/src/platform/transition/put_document.rs index fa85a30a0dc..75503ba1428 100644 --- a/packages/rs-sdk/src/platform/transition/put_document.rs +++ b/packages/rs-sdk/src/platform/transition/put_document.rs @@ -3,15 +3,18 @@ use super::validation::ensure_valid_state_transition_structure; use super::waitable::Waitable; use crate::platform::transition::put_settings::PutSettings; use crate::{Error, Sdk}; +// Transport-free helpers shared with embedders; the implementations moved to +// `dash-platform-queries`. +pub use dash_platform_queries::transition::put_document::{ + ensure_entropy_matches_document_id, prepare_document_for_transition, +}; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::DocumentType; use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters, INITIAL_REVISION}; use dpp::identity::signer::Signer; use dpp::identity::IdentityPublicKey; -use dpp::prelude::Identifier; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::StateTransition; @@ -162,162 +165,3 @@ impl> PutDocument for Document { Self::wait_for_response(sdk, state_transition, settings).await } } - -fn prepare_document_for_transition(document: &Document, document_type: &DocumentType) -> Document { - let mut document = document.clone(); - document_type - .as_ref() - .sanitize_document_properties(document.properties_mut()); - document -} - -/// Ensures a caller-supplied `entropy` derives the same document id already set -/// on a create document. -/// -/// A document-create state transition carries both the document id and the -/// entropy, and Drive recomputes the id from the entropy during -/// `advanced_structure` validation, rejecting the transition with -/// `InvalidDocumentTransitionIdError` when they disagree. Because -/// [`PutDocument::put_to_platform`] trusts the caller's id verbatim in the -/// `Some(entropy)` arm, a two-phase caller whose id and entropy have drifted -/// would only discover the mismatch after paying (a bumped identity-contract -/// nonce). This check surfaces the mismatch locally before broadcasting. -fn ensure_entropy_matches_document_id( - contract_id: &Identifier, - owner_id: &Identifier, - document_type_name: &str, - entropy: &[u8; 32], - document_id: Identifier, -) -> Result<(), Error> { - let expected_id = Document::generate_document_id_v0( - contract_id, - owner_id, - document_type_name, - entropy.as_slice(), - ); - if expected_id != document_id { - return Err(Error::Generic(format!( - "document id {document_id} does not match the id {expected_id} derived from the \ - supplied entropy; the entropy must be the one used to generate the document id" - ))); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use dpp::data_contract::config::DataContractConfig; - use dpp::document::DocumentV0; - use dpp::platform_value::{platform_value, Value}; - use dpp::version::PlatformVersion; - use std::collections::BTreeMap; - - fn contract_id() -> Identifier { - Identifier::from([1u8; 32]) - } - - fn owner_id() -> Identifier { - Identifier::from([2u8; 32]) - } - - #[test] - fn matching_entropy_and_id_pass() { - let entropy = [7u8; 32]; - let id = Document::generate_document_id_v0( - &contract_id(), - &owner_id(), - "contactRequest", - entropy.as_slice(), - ); - - ensure_entropy_matches_document_id( - &contract_id(), - &owner_id(), - "contactRequest", - &entropy, - id, - ) - .expect("id derived from the supplied entropy must be accepted"); - } - - #[test] - fn mismatched_entropy_and_id_error_before_broadcast() { - // The id was derived from E1, but the caller passes E2 != E1 (mirroring - // the very drift consensus rejects with InvalidDocumentTransitionIdError). - let entropy_used = [1u8; 32]; - let id = Document::generate_document_id_v0( - &contract_id(), - &owner_id(), - "contactRequest", - entropy_used.as_slice(), - ); - - let different_entropy = [2u8; 32]; - let result = ensure_entropy_matches_document_id( - &contract_id(), - &owner_id(), - "contactRequest", - &different_entropy, - id, - ); - - assert!( - matches!(result, Err(Error::Generic(_))), - "a document id derived from a different entropy must be rejected locally" - ); - } - - #[test] - fn should_normalize_wasm_uint8_array_property_without_mutating_caller_document() { - let platform_version = PlatformVersion::latest(); - let config = DataContractConfig::default_for_version(platform_version) - .expect("should create default data contract config"); - let document_type = DocumentType::try_from_schema( - contract_id(), - 1, - config.version(), - "preorder", - platform_value!({ - "type": "object", - "properties": { - "saltedDomainHash": { - "type": "array", - "byteArray": true, - "minItems": 32_u32, - "maxItems": 32_u32, - "position": 0 - } - }, - "required": ["saltedDomainHash"], - "additionalProperties": false, - }), - None, - &BTreeMap::new(), - &config, - false, - &mut Vec::new(), - platform_version, - ) - .expect("should create DPNS-like document type"); - let integer_array = Value::Array(vec![Value::U64(7); 32]); - let document = Document::V0(DocumentV0 { - id: Identifier::new([3; 32]), - owner_id: owner_id(), - properties: BTreeMap::from([("saltedDomainHash".to_string(), integer_array.clone())]), - revision: Some(INITIAL_REVISION), - ..Default::default() - }); - - let prepared = prepare_document_for_transition(&document, &document_type); - - assert_eq!( - prepared.properties().get("saltedDomainHash"), - Some(&Value::Bytes32([7; 32])) - ); - assert_eq!( - document.properties().get("saltedDomainHash"), - Some(&integer_array) - ); - } -} From 13472b64b902d4975c643535edff0afd0d8fb1c7 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 18 Aug 2026 14:01:25 -0500 Subject: [PATCH 8/8] feat(sdk): add transport-free CXX bindings --- .../package-filters/rs-packages-direct.yml | 3 + .../rs-packages-no-workflows.yml | 9 + .github/package-filters/rs-packages.yml | 10 + .../tests-rs-nightly-long-running.yml | 1 + .github/workflows/tests-rs-workspace.yml | 8 +- Cargo.lock | 171 +- Cargo.toml | 1 + Dockerfile | 4 + packages/check-features/src/main.rs | 1 + packages/rs-platform-cxx/Cargo.toml | 47 + packages/rs-platform-cxx/README.md | 31 + packages/rs-platform-cxx/build.rs | 9 + packages/rs-platform-cxx/ffi.h | 6 + packages/rs-platform-cxx/install.sh | 43 + packages/rs-platform-cxx/signer.h | 65 + packages/rs-platform-cxx/src/decode.rs | 168 + packages/rs-platform-cxx/src/lib.rs | 711 +++ packages/rs-platform-cxx/src/provider.rs | 181 + packages/rs-platform-cxx/src/st.rs | 623 +++ packages/rs-platform-cxx/src/types.rs | 112 + packages/rs-platform-cxx/src/verify.rs | 307 ++ .../rs-platform-cxx/standalone/.gitignore | 1 + .../rs-platform-cxx/standalone/Cargo.lock | 3960 +++++++++++++++++ .../rs-platform-cxx/standalone/Cargo.toml | 17 + .../rs-platform-cxx/standalone/src/lib.rs | 1 + packages/rs-platform-cxx/test-cxx-link.sh | 29 + .../test_data/dpp_identity_vectors.json | 52 + .../test_data/dpp_st_vectors.json | 233 + .../test_data/drive_query_vectors.json | 480 ++ .../test_data/quorum_sig_vectors.json | 125 + packages/rs-platform-cxx/tests/cxx_smoke.cc | 13 + packages/rs-platform-cxx/tests/decoders.rs | 161 + packages/rs-platform-cxx/tests/from_proof.rs | 690 +++ packages/rs-platform-cxx/tests/signing.rs | 407 ++ 34 files changed, 8661 insertions(+), 19 deletions(-) create mode 100644 packages/rs-platform-cxx/Cargo.toml create mode 100644 packages/rs-platform-cxx/README.md create mode 100644 packages/rs-platform-cxx/build.rs create mode 100644 packages/rs-platform-cxx/ffi.h create mode 100755 packages/rs-platform-cxx/install.sh create mode 100644 packages/rs-platform-cxx/signer.h create mode 100644 packages/rs-platform-cxx/src/decode.rs create mode 100644 packages/rs-platform-cxx/src/lib.rs create mode 100644 packages/rs-platform-cxx/src/provider.rs create mode 100644 packages/rs-platform-cxx/src/st.rs create mode 100644 packages/rs-platform-cxx/src/types.rs create mode 100644 packages/rs-platform-cxx/src/verify.rs create mode 100644 packages/rs-platform-cxx/standalone/.gitignore create mode 100644 packages/rs-platform-cxx/standalone/Cargo.lock create mode 100644 packages/rs-platform-cxx/standalone/Cargo.toml create mode 100644 packages/rs-platform-cxx/standalone/src/lib.rs create mode 100755 packages/rs-platform-cxx/test-cxx-link.sh create mode 100644 packages/rs-platform-cxx/test_data/dpp_identity_vectors.json create mode 100644 packages/rs-platform-cxx/test_data/dpp_st_vectors.json create mode 100644 packages/rs-platform-cxx/test_data/drive_query_vectors.json create mode 100644 packages/rs-platform-cxx/test_data/quorum_sig_vectors.json create mode 100644 packages/rs-platform-cxx/tests/cxx_smoke.cc create mode 100644 packages/rs-platform-cxx/tests/decoders.rs create mode 100644 packages/rs-platform-cxx/tests/from_proof.rs create mode 100644 packages/rs-platform-cxx/tests/signing.rs diff --git a/.github/package-filters/rs-packages-direct.yml b/.github/package-filters/rs-packages-direct.yml index 441c8137023..3f5df68b576 100644 --- a/.github/package-filters/rs-packages-direct.yml +++ b/.github/package-filters/rs-packages-direct.yml @@ -118,6 +118,9 @@ platform-encryption: dash-platform-queries: - packages/dash-platform-queries/** +dash-platform-cxx: + - packages/rs-platform-cxx/** + dash-sdk: - packages/rs-sdk/** diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 90835d0429f..594d5b63ed0 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -130,6 +130,15 @@ platform-encryption: &platform_encryption dash-platform-queries: &platform_queries - packages/dash-platform-queries/** +dash-platform-cxx: &platform_cxx + - packages/rs-platform-cxx/** + - *platform_queries + - *context_provider + - packages/rs-drive-proof-verifier/** + - *dpp + - *drive + - *dapi_grpc + dash-sdk: &sdk - packages/rs-drive-proof-verifier/** - packages/rs-sdk/** diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index 6fae2aa84ab..7621be08392 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -155,6 +155,16 @@ dash-platform-queries: &platform_queries - .github/workflows/tests* - packages/dash-platform-queries/** +dash-platform-cxx: &platform_cxx + - .github/workflows/tests* + - packages/rs-platform-cxx/** + - *platform_queries + - *context_provider + - packages/rs-drive-proof-verifier/** + - *dpp + - *drive + - *dapi_grpc + dash-sdk: &sdk - .github/workflows/tests* - packages/rs-drive-proof-verifier/** diff --git a/.github/workflows/tests-rs-nightly-long-running.yml b/.github/workflows/tests-rs-nightly-long-running.yml index f65b87a37ec..b7d3a45690e 100644 --- a/.github/workflows/tests-rs-nightly-long-running.yml +++ b/.github/workflows/tests-rs-nightly-long-running.yml @@ -30,6 +30,7 @@ jobs: drive-abci, drive-proof-verifier, dash-platform-queries, + dash-platform-cxx, ] steps: - name: Check out repo diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 1c159c2c706..136815a79c5 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -193,6 +193,11 @@ jobs: cargo install cargo-machete 2>/dev/null || true cargo machete + - name: Check Platform CXX embedder + run: | + cargo test -p dash-platform-cxx --locked + CARGO_TARGET_DIR=target packages/rs-platform-cxx/test-cxx-link.sh + # The transport-free cuts are how embedders with their own networking # (Dash Core's platform GUI, explorers) consume verification: feature # unification hides regressions in whole-workspace builds, so check the @@ -203,13 +208,14 @@ jobs: cargo check -p dapi-grpc --no-default-features --features core,platform,client --locked cargo check -p drive-proof-verifier --locked cargo check -p dash-platform-queries --locked + cargo check -p dash-platform-cxx --locked # Native graphs: assert the networking transport stack stays out. # `tonic` itself is present (dapi-grpc's generated client types) but # without its transport feature — which is exactly what the absence # of hyper/rustls/tower proves. tokio is deliberately NOT asserted # absent: dash-context-provider depends on dash-async, which uses it # on native targets, and that edge predates the queries-crate split. - for native_package in drive-proof-verifier dash-platform-queries; do + for native_package in drive-proof-verifier dash-platform-queries dash-platform-cxx; do for banned in hyper rustls tower; do if cargo tree -p "$native_package" -e normal -i "$banned" 2>/dev/null | grep -q .; then echo "::error::$banned leaked into $native_package's dependency tree" diff --git a/Cargo.lock b/Cargo.lock index f0a20a6cd53..090ad1fff4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,7 +576,7 @@ dependencies = [ "bitflags 2.13.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -1211,6 +1211,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1229,7 +1240,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1549,6 +1560,68 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cxx" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash 0.2.0", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap 2.14.0", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap 2.14.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap 2.14.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "dapi-grpc" version = "4.2.0-dev.1" @@ -1693,6 +1766,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "dash-platform-cxx" +version = "4.2.0-dev.1" +dependencies = [ + "async-trait", + "ciborium", + "cxx", + "cxx-build", + "dapi-grpc", + "dash-context-provider", + "dash-platform-queries", + "dpp", + "drive-proof-verifier", + "futures", + "hex", + "platform-version", + "prost 0.14.4", + "serde", + "serde_json", +] + [[package]] name = "dash-platform-macros" version = "4.2.0-dev.1" @@ -2494,7 +2588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2555,7 +2649,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3608,7 +3702,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "system-configuration", "tokio", "tower-service", @@ -3859,7 +3953,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4288,6 +4382,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -5560,8 +5663,8 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.4.1", - "itertools 0.10.5", + "heck 0.5.0", + "itertools 0.14.0", "log", "multimap", "petgraph", @@ -5582,7 +5685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5595,7 +5698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5731,7 +5834,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.5.10", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -5769,9 +5872,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.4", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6579,7 +6682,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6592,7 +6695,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6651,7 +6754,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6771,6 +6874,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + [[package]] name = "scrypt" version = "0.11.0" @@ -7448,6 +7557,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -7511,7 +7631,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7570,6 +7690,15 @@ dependencies = [ "zip 8.6.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termtree" version = "0.5.1" @@ -8386,6 +8515,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -8960,7 +9095,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 23dcd8d051c..4124ecf2c9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "packages/wasm-sdk", "packages/rs-unified-sdk-ffi", "packages/rs-unified-sdk-jni", + "packages/rs-platform-cxx", "packages/rs-scripts", ] diff --git a/Dockerfile b/Dockerfile index 846150203f7..67be321482a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -416,6 +416,7 @@ COPY --parents \ packages/wasm-drive-verify \ packages/rs-dapi-client \ packages/dash-platform-queries \ + packages/rs-platform-cxx \ packages/rs-sdk \ packages/rs-sdk-ffi \ packages/rs-unified-sdk-ffi \ @@ -543,6 +544,7 @@ COPY --parents \ packages/wasm-drive-verify \ packages/rs-dapi-client \ packages/dash-platform-queries \ + packages/rs-platform-cxx \ packages/rs-sdk \ packages/rs-sdk-ffi \ packages/rs-unified-sdk-ffi \ @@ -693,6 +695,7 @@ COPY --parents \ packages/rs-drive \ packages/rs-drive-proof-verifier \ packages/dash-platform-queries \ + packages/rs-platform-cxx \ packages/rs-sdk \ packages/rs-sdk-trusted-context-provider \ # Common @@ -966,6 +969,7 @@ COPY --parents \ packages/wasm-drive-verify \ packages/rs-dapi-client \ packages/dash-platform-queries \ + packages/rs-platform-cxx \ packages/rs-sdk \ packages/rs-sdk-ffi \ packages/rs-unified-sdk-ffi \ diff --git a/packages/check-features/src/main.rs b/packages/check-features/src/main.rs index 8cba6485410..a89521780c1 100644 --- a/packages/check-features/src/main.rs +++ b/packages/check-features/src/main.rs @@ -11,6 +11,7 @@ fn main() { ("rs-drive-proof-verifier", vec![]), ("rs-platform-wallet", vec![]), ("dash-platform-queries", vec![]), + ("rs-platform-cxx", vec![]), ]; for (specific_crate, to_ignore) in crates { diff --git a/packages/rs-platform-cxx/Cargo.toml b/packages/rs-platform-cxx/Cargo.toml new file mode 100644 index 00000000000..00a9c0433a7 --- /dev/null +++ b/packages/rs-platform-cxx/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "dash-platform-cxx" +version.workspace = true +authors = ["Dash Core Group "] +edition = "2021" +rust-version.workspace = true +license = "MIT" +description = "Transport-free CXX bindings for embedding Dash Platform" + +[lib] +name = "dash_platform_cxx" +crate-type = ["staticlib", "rlib"] + +[dependencies] +cxx = "=1.0.198" +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ + "platform", + "client", +] } +dash-context-provider = { path = "../rs-context-provider", default-features = false } +dash-platform-queries = { path = "../dash-platform-queries", default-features = false } +drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false } +dpp = { path = "../rs-dpp", default-features = false, features = [ + "state-transitions", + "state-transition-signing", + "identity-serialization", + "identity-hashing", + "bls-signatures", + "dpns-contract", + "dashpay-contract", +] } +platform-version = { path = "../rs-platform-version" } +prost = "0.14" +hex = "0.4" +futures = "0.3" +async-trait = "0.1" + +[build-dependencies] +cxx-build = "=1.0.198" + +[features] +default = [] + +[dev-dependencies] +serde_json = "1" +serde = { version = "1", features = ["derive"] } +ciborium = "0.2" diff --git a/packages/rs-platform-cxx/README.md b/packages/rs-platform-cxx/README.md new file mode 100644 index 00000000000..2cbefd50b25 --- /dev/null +++ b/packages/rs-platform-cxx/README.md @@ -0,0 +1,31 @@ +# Dash Platform CXX + +`dash-platform-cxx` is the transport-free C++ embedding surface for Dash +Platform. It exposes proof verification, DPP decoding, and state-transition +construction while leaving transport, endpoint selection, quorum-state +synchronization, and private-key custody with the embedding application. + +The Rust implementation and CXX schema live in this package. Consumers build +the `standalone` manifest as one static library and include +`dash/platform/ffi.h` and `dash/platform/signer.h` from the generated CXX +include tree. The standalone manifest has its own lockfile so embedders can +vendor this package's dependency closure without vendoring unrelated Platform +workspace packages. + +Build and install the standalone archive with: + +```sh +CARGO_TARGET_DIR=/path/to/target \ + cargo build --manifest-path standalone/Cargo.toml --locked --release +CARGO_TARGET_DIR=/path/to/target ./install.sh /path/to/prefix +``` + +The installed interface consists of `include/dash/platform/ffi.h`, +`include/dash/platform/signer.h`, the generated CXX headers they require, and +`lib/libdash_platform_cxx.a`. The embedding application remains responsible +for the platform-specific system libraries required by a Rust static library. + +The normal dependency graph intentionally excludes `rs-dapi-client` and the +Hyper/Rustls/Tower transport stack. Generated protobuf client types, Tokio +utilities, and the context-provider abstraction remain available without a +native DAPI transport. diff --git a/packages/rs-platform-cxx/build.rs b/packages/rs-platform-cxx/build.rs new file mode 100644 index 00000000000..51b25e14e72 --- /dev/null +++ b/packages/rs-platform-cxx/build.rs @@ -0,0 +1,9 @@ +fn main() { + cxx_build::CFG.include_prefix = "dash/platform"; + cxx_build::bridge("src/lib.rs") + .std("c++20") + .compile("dash-platform-cxx-bridge"); + + println!("cargo:rerun-if-changed=src/"); + println!("cargo:rerun-if-changed=signer.h"); +} diff --git a/packages/rs-platform-cxx/ffi.h b/packages/rs-platform-cxx/ffi.h new file mode 100644 index 00000000000..2763b3fb8e2 --- /dev/null +++ b/packages/rs-platform-cxx/ffi.h @@ -0,0 +1,6 @@ +#ifndef DASH_PLATFORM_CXX_FFI_H +#define DASH_PLATFORM_CXX_FFI_H + +#include + +#endif // DASH_PLATFORM_CXX_FFI_H diff --git a/packages/rs-platform-cxx/install.sh b/packages/rs-platform-cxx/install.sh new file mode 100755 index 00000000000..237e2c40dc0 --- /dev/null +++ b/packages/rs-platform-cxx/install.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "$#" -ne 1 ]]; then + echo "usage: $0 PREFIX" >&2 + exit 1 +fi + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +workspace_dir="$(cd "${package_dir}/../.." && pwd)" +target_dir="${CARGO_TARGET_DIR:-${workspace_dir}/target/platform-cxx-standalone}" +profile="${CARGO_PROFILE:-release}" +artifact_dir="${target_dir}" +if [[ -n "${CARGO_BUILD_TARGET:-}" ]]; then + artifact_dir="${artifact_dir}/${CARGO_BUILD_TARGET}" +fi +artifact_dir="${artifact_dir}/${profile}" +prefix="$1" + +archive="${artifact_dir}/libdash_platform_cxx_bundle.a" +if [[ ! -f "${archive}" ]]; then + echo "missing standalone archive: ${archive}" >&2 + exit 1 +fi + +shopt -s nullglob +bridge_headers=("${artifact_dir}"/build/dash-platform-cxx-*/out/cxxbridge/include/dash/platform/src/lib.rs.h) +runtime_headers=("${artifact_dir}"/build/dash-platform-cxx-*/out/cxxbridge/include/rust/cxx.h) +if [[ "${#bridge_headers[@]}" -ne 1 || "${#runtime_headers[@]}" -ne 1 ]]; then + echo "expected exactly one generated CXX header set under ${artifact_dir}" >&2 + exit 1 +fi + +install -d \ + "${prefix}/include/dash/platform/src" \ + "${prefix}/include/rust" \ + "${prefix}/lib" +install -m 0644 "${package_dir}/ffi.h" "${prefix}/include/dash/platform/ffi.h" +install -m 0644 "${package_dir}/signer.h" "${prefix}/include/dash/platform/signer.h" +install -m 0644 "${bridge_headers[0]}" "${prefix}/include/dash/platform/src/lib.rs.h" +install -m 0644 "${runtime_headers[0]}" "${prefix}/include/rust/cxx.h" +install -m 0644 "${archive}" "${prefix}/lib/libdash_platform_cxx.a" diff --git a/packages/rs-platform-cxx/signer.h b/packages/rs-platform-cxx/signer.h new file mode 100644 index 00000000000..24daa0badd3 --- /dev/null +++ b/packages/rs-platform-cxx/signer.h @@ -0,0 +1,65 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_PLATFORM_CXX_SIGNER_H +#define DASH_PLATFORM_CXX_SIGNER_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace platform_ffi { + +//! Signer handed by reference into the Rust state-transition builders. Rust +//! calls SignDigestForKey with the id of the +//! identity key being signed (or ASSET_LOCK_KEY_ID for the one-time +//! asset-lock key of an identity registration) and the 32-byte double-SHA256 +//! digest of the transition's signable bytes; the callback must return a +//! 65-byte compact recoverable ECDSA signature. Private keys never cross +//! the FFI boundary. +class WalletSigner +{ +public: + //! Reserved key id used for asset-lock proof signing (u32::MAX). + static constexpr uint32_t ASSET_LOCK_KEY_ID{0xffffffff}; + + using SignFn = std::function& digest, + std::vector& sig_out)>; + + explicit WalletSigner(SignFn sign_fn) : m_sign_fn(std::move(sign_fn)) {} + + bool SignDigestForKey(uint32_t key_id, rust::Slice digest, + rust::Vec& sig_out) const + { + if (!m_sign_fn || digest.size() != 32) return false; + std::array digest_array; + std::copy(digest.begin(), digest.end(), digest_array.begin()); + std::vector signature; + // This is called from Rust frames; a C++ exception must not unwind + // through them (unsupported by cxx), so a throwing signer reads as a + // signing refusal instead. + try { + if (!m_sign_fn(key_id, digest_array, signature)) return false; + } catch (...) { + return false; + } + sig_out.clear(); + sig_out.reserve(signature.size()); + std::copy(signature.begin(), signature.end(), std::back_inserter(sig_out)); + return true; + } + +private: + SignFn m_sign_fn; +}; + +} // namespace platform_ffi + +#endif // DASH_PLATFORM_CXX_SIGNER_H diff --git a/packages/rs-platform-cxx/src/decode.rs b/packages/rs-platform-cxx/src/decode.rs new file mode 100644 index 00000000000..e579ce98faa --- /dev/null +++ b/packages/rs-platform-cxx/src/decode.rs @@ -0,0 +1,168 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! DPP decoders for C++ identity and document value types, built on the real +//! rs-dpp deserializers. + +use std::collections::BTreeMap; + +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::{Document, DocumentV0Getters}; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::{Identity, IdentityPublicKey}; +use dpp::platform_value::Value; +use dpp::serialization::PlatformDeserializable; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use platform_version::version::PlatformVersion; + +use crate::types::{ContactRequest, DpnsName, IdentityInfo, KeyInfo, Profile}; + +/// Flattens a dpp key into the FFI form. +pub(crate) fn key_info(key: &IdentityPublicKey) -> KeyInfo { + KeyInfo { + id: key.id(), + purpose: key.purpose() as u8, + security_level: key.security_level() as u8, + key_type: key.key_type() as u8, + read_only: key.read_only(), + data: key.data().to_vec(), + disabled_at: key.disabled_at(), + } +} + +/// Flattens a dpp identity into the FFI form. +pub(crate) fn identity_info(identity: &Identity) -> IdentityInfo { + IdentityInfo { + id: identity.id().to_buffer(), + balance: identity.balance(), + revision: identity.revision(), + keys: identity.public_keys().values().map(key_info).collect(), + } +} + +pub fn decode_identity(bytes: &[u8]) -> Result { + let identity = + Identity::deserialize_from_bytes(bytes).map_err(|e| format!("bad identity: {e}"))?; + Ok(identity_info(&identity)) +} + +pub fn decode_identity_public_key(bytes: &[u8]) -> Result { + let key = IdentityPublicKey::deserialize_from_bytes(bytes) + .map_err(|e| format!("bad identity public key: {e}"))?; + Ok(key_info(&key)) +} + +fn decode_document( + bytes: &[u8], + contract: SystemDataContract, + document_type_name: &str, +) -> Result { + use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; + let version = PlatformVersion::latest(); + let contract = load_system_data_contract(contract, version) + .map_err(|e| format!("unable to load system data contract: {e}"))?; + let document_type = contract + .document_type_for_name(document_type_name) + .map_err(|e| format!("unknown document type {document_type_name}: {e}"))?; + Document::from_bytes(bytes, document_type, version) + .map_err(|e| format!("bad {document_type_name} document: {e}")) +} + +fn get_str(properties: &BTreeMap, name: &str) -> String { + match properties.get(name) { + Some(Value::Text(text)) => text.clone(), + _ => String::new(), + } +} + +fn get_bytes(properties: &BTreeMap, name: &str) -> Vec { + match properties.get(name) { + Some(Value::Bytes(bytes)) => bytes.clone(), + Some(Value::Bytes20(bytes)) => bytes.to_vec(), + Some(Value::Bytes32(bytes)) => bytes.to_vec(), + Some(Value::Bytes36(bytes)) => bytes.to_vec(), + Some(Value::Identifier(id)) => id.to_vec(), + _ => Vec::new(), + } +} + +fn get_u32(properties: &BTreeMap, name: &str) -> Result { + let Some(value) = properties.get(name) else { + return Ok(0); + }; + value + .clone() + .into_integer::() + .map_err(|e| format!("property {name} is not a u32: {e}")) +} + +fn get_identifier(value: Option<&Value>) -> Option<[u8; 32]> { + match value { + Some(Value::Identifier(id)) => Some(*id), + Some(Value::Bytes32(bytes)) => Some(*bytes), + Some(Value::Bytes(bytes)) => bytes.as_slice().try_into().ok(), + _ => None, + } +} + +pub fn decode_dpns_domain(doc_bytes: &[u8]) -> Result { + let document = decode_document(doc_bytes, SystemDataContract::DPNS, "domain")?; + let properties = document.properties(); + let identity = properties + .get("records") + .and_then(|records| match records { + Value::Map(map) => map + .iter() + .find(|(key, _)| matches!(key, Value::Text(text) if text == "identity")) + .map(|(_, value)| value), + _ => None, + }) + .and_then(|value| get_identifier(Some(value))) + .ok_or("DPNS domain document has no records.identity")?; + Ok(DpnsName { + label: get_str(properties, "label"), + normalized_label: get_str(properties, "normalizedLabel"), + parent_domain: get_str(properties, "normalizedParentDomainName"), + identity, + document_id: document.id().to_buffer(), + owner_id: document.owner_id().to_buffer(), + }) +} + +pub fn decode_dashpay_profile(doc_bytes: &[u8]) -> Result { + let document = decode_document(doc_bytes, SystemDataContract::Dashpay, "profile")?; + let properties = document.properties(); + Ok(Profile { + document_id: document.id().to_buffer(), + owner_id: document.owner_id().to_buffer(), + display_name: get_str(properties, "displayName"), + public_message: get_str(properties, "publicMessage"), + avatar_url: get_str(properties, "avatarUrl"), + avatar_hash: get_bytes(properties, "avatarHash"), + avatar_fingerprint: get_bytes(properties, "avatarFingerprint"), + created_at: document.created_at().unwrap_or(0), + updated_at: document.updated_at().unwrap_or(0), + revision: document.revision().unwrap_or(0), + }) +} + +pub fn decode_contact_request(doc_bytes: &[u8]) -> Result { + let document = decode_document(doc_bytes, SystemDataContract::Dashpay, "contactRequest")?; + let properties = document.properties(); + let to_user_id = get_identifier(properties.get("toUserId")) + .ok_or("contact request document has no toUserId")?; + Ok(ContactRequest { + document_id: document.id().to_buffer(), + owner_id: document.owner_id().to_buffer(), + to_user_id, + encrypted_public_key: get_bytes(properties, "encryptedPublicKey"), + sender_key_index: get_u32(properties, "senderKeyIndex")?, + recipient_key_index: get_u32(properties, "recipientKeyIndex")?, + account_reference: get_u32(properties, "accountReference")?, + encrypted_account_label: get_bytes(properties, "encryptedAccountLabel"), + core_height_created_at: document.created_at_core_block_height().unwrap_or(0), + created_at: document.created_at().unwrap_or(0), + }) +} diff --git a/packages/rs-platform-cxx/src/lib.rs b/packages/rs-platform-cxx/src/lib.rs new file mode 100644 index 00000000000..9f9f3f33cfe --- /dev/null +++ b/packages/rs-platform-cxx/src/lib.rs @@ -0,0 +1,711 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Transport-free Dash Platform client internals exposed to C++ embedders. +//! +//! - `verify`: Drive/GroveDB proof verification over DAPI wire bytes; +//! - `decode`: DPP decoders for identities and DPNS/DashPay documents; +//! - `st`: state-transition construction with callback-based signing. +//! +//! The `#[cxx::bridge]` below exposes thin adapters over those modules. +//! Signing crosses the FFI as a digest callback (`WalletSigner`, +//! `dash/platform/signer.h`) so private keys never leave the embedder. + +pub mod decode; +pub mod provider; +pub mod st; +pub mod types; +pub mod verify; + +use types::{BuiltTransition, KeyInfo}; + +#[allow(clippy::too_many_arguments)] +#[cxx::bridge(namespace = "platform_ffi")] +mod ffi { + /// A byte vector, wrapped because cxx shared structs cannot hold + /// Vec>. + #[derive(Clone)] + struct FfiBytes { + data: Vec, + } + + /// One identity public key. `has_disabled_at == false` means the key is + /// not disabled. + #[derive(Clone)] + struct FfiIdentityKey { + id: u32, + purpose: u8, + security_level: u8, + key_type: u8, + read_only: bool, + data: Vec, + has_disabled_at: bool, + disabled_at: u64, + } + + /// One contender of a contested resource: identity id and its vote + /// tally (when requested/available). + struct FfiContender { + identity: Vec, + has_votes: bool, + votes: u32, + } + + /// Decoded identity. + struct FfiIdentity { + id: Vec, + balance: u64, + revision: u64, + keys: Vec, + } + + /// Decoded DPNS domain document. + struct FfiDpnsName { + label: String, + normalized_label: String, + parent_domain: String, + identity: Vec, + document_id: Vec, + owner_id: Vec, + } + + /// Decoded DashPay profile document. Empty vectors/strings and zero + /// timestamps mean the field is absent. + struct FfiProfile { + document_id: Vec, + owner_id: Vec, + display_name: String, + public_message: String, + avatar_url: String, + avatar_hash: Vec, + avatar_fingerprint: Vec, + created_at: u64, + updated_at: u64, + revision: u64, + } + + /// Decoded DashPay contactRequest document. + struct FfiContactRequest { + owner_id: Vec, + to_user_id: Vec, + encrypted_public_key: Vec, + sender_key_index: u32, + recipient_key_index: u32, + account_reference: u32, + encrypted_account_label: Vec, + core_height_created_at: u32, + created_at: u64, + document_id: Vec, + } + + /// A quorum BLS public key pushed from the node's LLMQ store. + /// `quorum_hash` (32 bytes) is in the byte order DAPI proofs carry it + /// (display order); `pubkey` is the 48-byte basic-scheme public key. + #[derive(Clone)] + struct FfiQuorumKey { + quorum_hash: Vec, + pubkey: Vec, + } + + /// Authenticated ResponseMetadata fields of a verified response (the + /// quorum signature covers them via the StateId sign bytes). + #[derive(Clone)] + struct FfiMeta { + height: u64, + core_chain_locked_height: u32, + time_ms: u64, + protocol_version: u32, + chain_id: String, + } + + /// Verified optional u64 (nonce); `present == false` means proven + /// absent. + struct FfiVerifiedU64 { + present: bool, + value: u64, + meta: FfiMeta, + } + + /// Verified optional identity; `present == false` means proven absent. + struct FfiVerifiedIdentity { + present: bool, + identity: FfiIdentity, + meta: FfiMeta, + } + + /// Verified document query result (serialized documents; empty means + /// proven no matches). + struct FfiVerifiedDocs { + documents: Vec, + meta: FfiMeta, + } + + /// Verified contested-resource vote state. + struct FfiVerifiedContested { + /// False when the contest was cryptographically proven absent. + contest_found: bool, + contenders: Vec, + has_abstain: bool, + abstain_votes: u32, + has_lock: bool, + lock_votes: u32, + /// True once the poll finished (awarded or locked). + finished: bool, + locked: bool, + has_winner: bool, + winner: Vec, + finished_at_time_ms: u64, + meta: FfiMeta, + } + + /// A public key to register with a new identity. + struct FfiNewIdentityKey { + id: u32, + purpose: u8, + security_level: u8, + /// Compressed secp256k1 public key (33 bytes). + pubkey: Vec, + } + + /// A built, signed state transition. `hash` is sha256(bytes), the wait + /// handle for waitForStateTransitionResult. + struct FfiBuiltTransition { + bytes: Vec, + hash: Vec, + } + + unsafe extern "C++" { + include!("dash/platform/signer.h"); + + /// Wallet-backed signer. `SignDigestForKey` signs a 32-byte digest + /// with the wallet key identified by `key_id` (`u32::MAX` selects + /// the one-time asset-lock key of an identity registration) and + /// returns a 65-byte compact recoverable ECDSA signature, or false + /// on failure. + type WalletSigner; + fn SignDigestForKey( + self: &WalletSigner, + key_id: u32, + digest: &[u8], + sig_out: &mut Vec, + ) -> bool; + } + + extern "Rust" { + // --- Node-local verification context ---------------------------- + /// Sets the network ("main"/"test"/"regtest"/"devnet") and the + /// Platform activation core height (0 = unknown; only consulted by + /// query paths that require it). + fn set_context(network_id: &str, platform_activation_height: u32) -> Result<()>; + /// Replaces the stored quorum keys of `quorum_type` with `keys`. + fn update_quorum_keys(quorum_type: u8, keys: Vec) -> Result<()>; + + // --- FromProof verification over (request, response) bytes ------ + fn verify_get_identity_nonce(request: &[u8], response: &[u8]) -> Result; + fn verify_get_identity_contract_nonce( + request: &[u8], + response: &[u8], + ) -> Result; + fn verify_get_identity(request: &[u8], response: &[u8]) -> Result; + fn verify_get_identity_by_pubkey_hash( + request: &[u8], + response: &[u8], + ) -> Result; + fn verify_get_documents(request: &[u8], response: &[u8]) -> Result; + fn verify_get_contested_vote_state( + request: &[u8], + response: &[u8], + ) -> Result; + + // --- DPP decoders ----------------------------------------------- + fn decode_identity(bytes: &[u8]) -> Result; + fn decode_identity_public_key(bytes: &[u8]) -> Result; + fn decode_dpns_domain(doc_bytes: &[u8]) -> Result; + fn decode_dashpay_profile(doc_bytes: &[u8]) -> Result; + fn decode_contact_request(doc_bytes: &[u8]) -> Result; + + // --- State transitions ------------------------------------------ + fn st_build_dpns_preorder( + identity_id: &[u8], + identity_contract_nonce: u64, + label: &str, + preorder_salt: &[u8], + signature_public_key_id: u32, + key: FfiIdentityKey, + signer: &WalletSigner, + ) -> Result; + fn st_build_dpns_domain( + identity_id: &[u8], + identity_contract_nonce: u64, + label: &str, + normalized_label: &str, + parent_domain: &str, + preorder_salt: &[u8], + signature_public_key_id: u32, + key: FfiIdentityKey, + signer: &WalletSigner, + ) -> Result; + fn st_build_profile( + identity_id: &[u8], + identity_contract_nonce: u64, + display_name: &str, + public_message: &str, + avatar_url: &str, + avatar_hash: &[u8], + avatar_fingerprint: &[u8], + revision: u64, + has_existing_doc_id: bool, + existing_document_id: &[u8], + entropy: &[u8], + signature_public_key_id: u32, + key: FfiIdentityKey, + signer: &WalletSigner, + ) -> Result; + fn st_build_contact_request( + identity_id: &[u8], + identity_contract_nonce: u64, + to_user_id: &[u8], + encrypted_public_key: &[u8], + sender_key_index: u32, + recipient_key_index: u32, + account_reference: u32, + encrypted_account_label: &[u8], + entropy: &[u8], + signature_public_key_id: u32, + key: FfiIdentityKey, + signer: &WalletSigner, + ) -> Result; + fn st_build_identity_create( + is_instant: bool, + transaction: &[u8], + instant_lock: &[u8], + output_index: u32, + core_chain_locked_height: u32, + out_point: &[u8], + keys: Vec, + signer: &WalletSigner, + ) -> Result; + } +} + +// --------------------------------------------------------------------------- +// Conversions between the plain-Rust core types and the flat FFI structs. +// --------------------------------------------------------------------------- + +fn ffi_key(key: &KeyInfo) -> ffi::FfiIdentityKey { + ffi::FfiIdentityKey { + id: key.id, + purpose: key.purpose, + security_level: key.security_level, + key_type: key.key_type, + read_only: key.read_only, + data: key.data.clone(), + has_disabled_at: key.disabled_at.is_some(), + disabled_at: key.disabled_at.unwrap_or(0), + } +} + +fn key_info(key: &ffi::FfiIdentityKey) -> KeyInfo { + KeyInfo { + id: key.id, + purpose: key.purpose, + security_level: key.security_level, + key_type: key.key_type, + read_only: key.read_only, + data: key.data.clone(), + disabled_at: key.has_disabled_at.then_some(key.disabled_at), + } +} + +fn ffi_built(built: BuiltTransition) -> ffi::FfiBuiltTransition { + ffi::FfiBuiltTransition { + bytes: built.bytes, + hash: built.hash.to_vec(), + } +} + +fn ffi_meta(meta: types::Meta) -> ffi::FfiMeta { + ffi::FfiMeta { + height: meta.height, + core_chain_locked_height: meta.core_chain_locked_height, + time_ms: meta.time_ms, + protocol_version: meta.protocol_version, + chain_id: meta.chain_id, + } +} + +fn ffi_verified_u64((value, meta): (Option, types::Meta)) -> ffi::FfiVerifiedU64 { + ffi::FfiVerifiedU64 { + present: value.is_some(), + value: value.unwrap_or(0), + meta: ffi_meta(meta), + } +} + +fn ffi_identity(identity: &types::IdentityInfo) -> ffi::FfiIdentity { + ffi::FfiIdentity { + id: identity.id.to_vec(), + balance: identity.balance, + revision: identity.revision, + keys: identity.keys.iter().map(ffi_key).collect(), + } +} + +fn ffi_verified_identity( + (identity, meta): (Option, types::Meta), +) -> ffi::FfiVerifiedIdentity { + ffi::FfiVerifiedIdentity { + present: identity.is_some(), + identity: identity + .as_ref() + .map(ffi_identity) + .unwrap_or_else(|| ffi::FfiIdentity { + id: Vec::new(), + balance: 0, + revision: 0, + keys: Vec::new(), + }), + meta: ffi_meta(meta), + } +} + +// --------------------------------------------------------------------------- +// Bridge implementations: verification context. +// --------------------------------------------------------------------------- + +fn set_context(network_id: &str, platform_activation_height: u32) -> Result<(), String> { + provider::set_context(network_id, platform_activation_height) +} + +fn update_quorum_keys(quorum_type: u8, keys: Vec) -> Result<(), String> { + let keys = keys + .into_iter() + .map(|key| { + Ok(provider::QuorumKey { + quorum_hash: types::id32(&key.quorum_hash, "quorum hash")?, + public_key: key.pubkey.as_slice().try_into().map_err(|_| { + format!( + "quorum public key must be 48 bytes, got {}", + key.pubkey.len() + ) + })?, + }) + }) + .collect::, String>>()?; + provider::update_quorum_keys(quorum_type, keys); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Bridge implementations: FromProof verification. +// --------------------------------------------------------------------------- + +fn verify_get_identity_nonce( + request: &[u8], + response: &[u8], +) -> Result { + verify::verify_get_identity_nonce(request, response).map(ffi_verified_u64) +} + +fn verify_get_identity_contract_nonce( + request: &[u8], + response: &[u8], +) -> Result { + verify::verify_get_identity_contract_nonce(request, response).map(ffi_verified_u64) +} + +fn verify_get_identity( + request: &[u8], + response: &[u8], +) -> Result { + verify::verify_get_identity(request, response).map(ffi_verified_identity) +} + +fn verify_get_identity_by_pubkey_hash( + request: &[u8], + response: &[u8], +) -> Result { + verify::verify_get_identity_by_pubkey_hash(request, response).map(ffi_verified_identity) +} + +fn verify_get_documents(request: &[u8], response: &[u8]) -> Result { + let (documents, meta) = verify::verify_get_documents(request, response)?; + Ok(ffi::FfiVerifiedDocs { + documents: documents + .into_iter() + .map(|data| ffi::FfiBytes { data }) + .collect(), + meta: ffi_meta(meta), + }) +} + +fn verify_get_contested_vote_state( + request: &[u8], + response: &[u8], +) -> Result { + let (state, meta) = verify::verify_get_contested_vote_state(request, response)?; + Ok(ffi::FfiVerifiedContested { + contest_found: state.contest_found, + contenders: state + .contenders + .iter() + .map(|(identity, votes)| ffi::FfiContender { + identity: identity.to_vec(), + has_votes: votes.is_some(), + votes: votes.unwrap_or(0), + }) + .collect(), + has_abstain: state.abstain_votes.is_some(), + abstain_votes: state.abstain_votes.unwrap_or(0), + has_lock: state.lock_votes.is_some(), + lock_votes: state.lock_votes.unwrap_or(0), + finished: state.finished, + locked: state.locked, + has_winner: state.winner.is_some(), + winner: state.winner.map(|id| id.to_vec()).unwrap_or_default(), + finished_at_time_ms: state.finished_at_time_ms, + meta: ffi_meta(meta), + }) +} + +fn decode_identity(bytes: &[u8]) -> Result { + decode::decode_identity(bytes).map(|identity| ffi_identity(&identity)) +} + +fn decode_identity_public_key(bytes: &[u8]) -> Result { + decode::decode_identity_public_key(bytes).map(|key| ffi_key(&key)) +} + +fn decode_dpns_domain(doc_bytes: &[u8]) -> Result { + let name = decode::decode_dpns_domain(doc_bytes)?; + Ok(ffi::FfiDpnsName { + label: name.label, + normalized_label: name.normalized_label, + parent_domain: name.parent_domain, + identity: name.identity.to_vec(), + document_id: name.document_id.to_vec(), + owner_id: name.owner_id.to_vec(), + }) +} + +fn decode_dashpay_profile(doc_bytes: &[u8]) -> Result { + let profile = decode::decode_dashpay_profile(doc_bytes)?; + Ok(ffi::FfiProfile { + document_id: profile.document_id.to_vec(), + owner_id: profile.owner_id.to_vec(), + display_name: profile.display_name, + public_message: profile.public_message, + avatar_url: profile.avatar_url, + avatar_hash: profile.avatar_hash, + avatar_fingerprint: profile.avatar_fingerprint, + created_at: profile.created_at, + updated_at: profile.updated_at, + revision: profile.revision, + }) +} + +fn decode_contact_request(doc_bytes: &[u8]) -> Result { + let request = decode::decode_contact_request(doc_bytes)?; + Ok(ffi::FfiContactRequest { + owner_id: request.owner_id.to_vec(), + to_user_id: request.to_user_id.to_vec(), + encrypted_public_key: request.encrypted_public_key, + sender_key_index: request.sender_key_index, + recipient_key_index: request.recipient_key_index, + account_reference: request.account_reference, + encrypted_account_label: request.encrypted_account_label, + core_height_created_at: request.core_height_created_at, + created_at: request.created_at, + document_id: request.document_id.to_vec(), + }) +} + +/// Shareable handle to the C++ signer. The bridge functions run the async +/// dpp builders to completion on the calling thread with a local executor, +/// so the signer is never actually accessed from another thread; the +/// `Send + Sync` assertion only satisfies dpp's `Signer: Send + Sync` +/// bound. +struct SignerHandle<'a>(&'a ffi::WalletSigner); +unsafe impl Send for SignerHandle<'_> {} +unsafe impl Sync for SignerHandle<'_> {} + +impl SignerHandle<'_> { + fn sign(&self, key_id: u32, digest: [u8; 32]) -> Option> { + let mut signature = Vec::new(); + self.0 + .SignDigestForKey(key_id, &digest, &mut signature) + .then_some(signature) + } +} + +fn check_key_id(signature_public_key_id: u32, key: &ffi::FfiIdentityKey) -> Result<(), String> { + if signature_public_key_id != key.id { + return Err(format!( + "signature public key id {signature_public_key_id} does not match key id {}", + key.id + )); + } + Ok(()) +} + +fn st_build_dpns_preorder( + identity_id: &[u8], + identity_contract_nonce: u64, + label: &str, + preorder_salt: &[u8], + signature_public_key_id: u32, + key: ffi::FfiIdentityKey, + signer: &ffi::WalletSigner, +) -> Result { + check_key_id(signature_public_key_id, &key)?; + let handle = SignerHandle(signer); + let sign_fn = |key_id: u32, digest: [u8; 32]| handle.sign(key_id, digest); + st::build_dpns_preorder( + identity_id, + identity_contract_nonce, + label, + preorder_salt, + &key_info(&key), + &sign_fn, + ) + .map(ffi_built) +} + +#[allow(clippy::too_many_arguments)] +fn st_build_dpns_domain( + identity_id: &[u8], + identity_contract_nonce: u64, + label: &str, + normalized_label: &str, + parent_domain: &str, + preorder_salt: &[u8], + signature_public_key_id: u32, + key: ffi::FfiIdentityKey, + signer: &ffi::WalletSigner, +) -> Result { + check_key_id(signature_public_key_id, &key)?; + let handle = SignerHandle(signer); + let sign_fn = |key_id: u32, digest: [u8; 32]| handle.sign(key_id, digest); + st::build_dpns_domain( + identity_id, + identity_contract_nonce, + label, + normalized_label, + parent_domain, + preorder_salt, + &key_info(&key), + &sign_fn, + ) + .map(ffi_built) +} + +#[allow(clippy::too_many_arguments)] +fn st_build_profile( + identity_id: &[u8], + identity_contract_nonce: u64, + display_name: &str, + public_message: &str, + avatar_url: &str, + avatar_hash: &[u8], + avatar_fingerprint: &[u8], + revision: u64, + has_existing_doc_id: bool, + existing_document_id: &[u8], + entropy: &[u8], + signature_public_key_id: u32, + key: ffi::FfiIdentityKey, + signer: &ffi::WalletSigner, +) -> Result { + check_key_id(signature_public_key_id, &key)?; + let handle = SignerHandle(signer); + let sign_fn = |key_id: u32, digest: [u8; 32]| handle.sign(key_id, digest); + st::build_profile( + identity_id, + identity_contract_nonce, + display_name, + public_message, + avatar_url, + avatar_hash, + avatar_fingerprint, + revision, + has_existing_doc_id.then_some(existing_document_id), + entropy, + &key_info(&key), + &sign_fn, + ) + .map(ffi_built) +} + +#[allow(clippy::too_many_arguments)] +fn st_build_contact_request( + identity_id: &[u8], + identity_contract_nonce: u64, + to_user_id: &[u8], + encrypted_public_key: &[u8], + sender_key_index: u32, + recipient_key_index: u32, + account_reference: u32, + encrypted_account_label: &[u8], + entropy: &[u8], + signature_public_key_id: u32, + key: ffi::FfiIdentityKey, + signer: &ffi::WalletSigner, +) -> Result { + check_key_id(signature_public_key_id, &key)?; + let handle = SignerHandle(signer); + let sign_fn = |key_id: u32, digest: [u8; 32]| handle.sign(key_id, digest); + st::build_contact_request( + identity_id, + identity_contract_nonce, + to_user_id, + encrypted_public_key, + sender_key_index, + recipient_key_index, + account_reference, + encrypted_account_label, + entropy, + &key_info(&key), + &sign_fn, + ) + .map(ffi_built) +} + +#[allow(clippy::too_many_arguments)] +fn st_build_identity_create( + is_instant: bool, + transaction: &[u8], + instant_lock: &[u8], + output_index: u32, + core_chain_locked_height: u32, + out_point: &[u8], + keys: Vec, + signer: &ffi::WalletSigner, +) -> Result { + let proof = if is_instant { + st::AssetLockProofInput::Instant { + transaction: transaction.to_vec(), + instant_lock: instant_lock.to_vec(), + output_index, + } + } else { + st::AssetLockProofInput::Chain { + core_chain_locked_height, + out_point: out_point + .try_into() + .map_err(|_| format!("outpoint must be 36 bytes, got {}", out_point.len()))?, + } + }; + let keys: Vec = keys + .into_iter() + .map(|key| st::NewIdentityKey { + id: key.id, + purpose: key.purpose, + security_level: key.security_level, + pubkey: key.pubkey, + }) + .collect(); + let handle = SignerHandle(signer); + let sign_fn = |key_id: u32, digest: [u8; 32]| handle.sign(key_id, digest); + st::build_identity_create(proof, &keys, &sign_fn).map(ffi_built) +} diff --git a/packages/rs-platform-cxx/src/provider.rs b/packages/rs-platform-cxx/src/provider.rs new file mode 100644 index 00000000000..733e540d1a6 --- /dev/null +++ b/packages/rs-platform-cxx/src/provider.rs @@ -0,0 +1,181 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! `dash_context_provider::ContextProvider` backed by node-local state. +//! +//! `FromProof` verification (drive-proof-verifier) resolves everything it +//! needs about the network through this trait: the BLS public key of the +//! quorum that signed a proof, and the data contracts referenced by document +//! queries. The embedder serves both from local knowledge: quorum keys are +//! pushed across the bridge from synced LLMQ data, and the supported document +//! queries use the pinned DPNS and DashPay system contracts compiled into dpp. +//! Proof verification therefore never performs network fetches. +//! +//! The provider is process-global and supports one Platform network at a time. + +use std::collections::HashMap; +use std::sync::{Arc, OnceLock, RwLock}; + +use dash_context_provider::{ContextProvider, ContextProviderError}; +use dpp::dashcore::Network; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::TokenConfiguration; +use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use platform_version::version::PlatformVersion; + +/// A quorum public key pushed from the node's LLMQ store. `quorum_hash` is +/// in the byte order DAPI proofs carry it (display order — the reverse of +/// the embedding application's internal hash order; the C++ adapter converts). +pub struct QuorumKey { + pub quorum_hash: [u8; 32], + pub public_key: [u8; 48], +} + +#[derive(Default)] +struct State { + /// (quorum_type, quorum_hash in proof byte order) -> BLS public key. + quorum_keys: HashMap<(u32, [u8; 32]), [u8; 48]>, + network: Option, + /// Core height at which Platform activated (mn_rr). Embedders that do not + /// use queries requiring this value may set it to 0. + platform_activation_height: CoreBlockHeight, + /// Lazily loaded pinned system contracts (DPNS, DashPay). + contracts: HashMap>, +} + +/// Process-global provider instance. +pub struct LocalContextProvider { + state: RwLock, +} + +static PROVIDER: OnceLock = OnceLock::new(); + +/// The process-global provider handed to every FromProof call. +pub fn provider() -> &'static LocalContextProvider { + PROVIDER.get_or_init(|| LocalContextProvider { + state: RwLock::new(State::default()), + }) +} + +/// Replaces the stored key set of `quorum_type` with `keys`. The C++ client +/// pushes the full active Platform-LLMQ set on every masternode-list / +/// quorum update, so replacement (not merge) keeps rotated-out quorums from +/// verifying new proofs forever. +pub fn update_quorum_keys(quorum_type: u8, keys: Vec) { + let mut state = provider().state.write().expect("provider lock poisoned"); + state + .quorum_keys + .retain(|(stored_type, _), _| *stored_type != u32::from(quorum_type)); + for key in keys { + state + .quorum_keys + .insert((u32::from(quorum_type), key.quorum_hash), key.public_key); + } +} + +/// Parses a Dash network id ("main", "test", "regtest", "devnet") into +/// the dashcore `Network` FromProof expects. +pub fn parse_network(network_id: &str) -> Result { + match network_id { + "main" => Ok(Network::Mainnet), + "test" => Ok(Network::Testnet), + "regtest" => Ok(Network::Regtest), + "devnet" => Ok(Network::Devnet), + other => Err(format!("unknown network id {other:?}")), + } +} + +/// Sets the network and the Platform activation core height (0 = unknown; +/// see `State::platform_activation_height`). +pub fn set_context(network_id: &str, platform_activation_height: u32) -> Result<(), String> { + let network = parse_network(network_id)?; + let mut state = provider().state.write().expect("provider lock poisoned"); + state.network = Some(network); + state.platform_activation_height = platform_activation_height; + Ok(()) +} + +/// The network set via `set_context`. +pub fn network() -> Result { + provider() + .state + .read() + .expect("provider lock poisoned") + .network + .ok_or_else(|| "platform bridge context not initialized (set_context)".to_string()) +} + +impl ContextProvider for LocalContextProvider { + fn get_data_contract( + &self, + id: &Identifier, + platform_version: &PlatformVersion, + ) -> Result>, ContextProviderError> { + if let Some(contract) = self + .state + .read() + .expect("provider lock poisoned") + .contracts + .get(id) + { + return Ok(Some(Arc::clone(contract))); + } + for system_contract in [SystemDataContract::DPNS, SystemDataContract::Dashpay] { + let contract = load_system_data_contract(system_contract, platform_version) + .map_err(|e| ContextProviderError::DataContractFailure(e.to_string()))?; + if contract.id() == *id { + let contract = Arc::new(contract); + self.state + .write() + .expect("provider lock poisoned") + .contracts + .insert(*id, Arc::clone(&contract)); + return Ok(Some(contract)); + } + } + Ok(None) + } + + fn get_token_configuration( + &self, + _token_id: &Identifier, + ) -> Result, ContextProviderError> { + Err(ContextProviderError::Generic( + "token configurations are not available through this binding".to_string(), + )) + } + + fn get_quorum_public_key( + &self, + quorum_type: u32, + quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], ContextProviderError> { + // The locally synced LLMQ store only tracks currently valid quorums, + // so the requested core height adds nothing to the lookup: a proof + // signed by a quorum the node no longer knows fails verification. + self.state + .read() + .expect("provider lock poisoned") + .quorum_keys + .get(&(quorum_type, quorum_hash)) + .copied() + .ok_or_else(|| { + ContextProviderError::InvalidQuorum(format!( + "no locally known quorum of type {} with hash {}", + quorum_type, + hex::encode(quorum_hash) + )) + }) + } + + fn get_platform_activation_height(&self) -> Result { + Ok(self + .state + .read() + .expect("provider lock poisoned") + .platform_activation_height) + } +} diff --git a/packages/rs-platform-cxx/src/st.rs b/packages/rs-platform-cxx/src/st.rs new file mode 100644 index 00000000000..c56c6ae7e3e --- /dev/null +++ b/packages/rs-platform-cxx/src/st.rs @@ -0,0 +1,623 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! DPP state-transition construction and signing for C++ embedders, built on +//! the real rs-dpp builders. +//! +//! Signing is delegated through a callback so private keys stay in the +//! wallet: the callback receives the key id being signed plus the +//! double-SHA256 digest of the transition's signable bytes and must return +//! a 65-byte compact recoverable ECDSA signature - exactly what +//! `dpp::dashcore::signer::sign` would produce from the raw key +//! (`sign(data, key) == sign_hash(sha256d(data), key)`). + +use std::collections::BTreeMap; + +use dash_platform_queries::dashpay::{ + build_contact_request_document, ContactRequestDocumentParams, +}; +use dash_platform_queries::dpns_usernames::build_dpns_preorder_and_domain_documents; +use dash_platform_queries::transition::put_document::{ + ensure_entropy_matches_document_id, prepare_document_for_transition, +}; +use dpp::address_funds::AddressWitness; +use dpp::dashcore::consensus::Decodable; +use dpp::dashcore::hashes::{sha256, Hash}; +use dpp::dashcore::{InstantLock, OutPoint, Transaction}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::DataContract; +use dpp::document::{Document, DocumentV0}; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; +use dpp::identity::signer::Signer; +use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; +use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; +use dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; +use dpp::platform_value::{BinaryData, Value}; +use dpp::prelude::{AssetLockProof, Identifier}; +use dpp::serialization::{PlatformSerializable, Signable}; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0; +use dpp::state_transition::public_key_in_creation::accessors::{ + IdentityPublicKeyInCreationV0Getters, IdentityPublicKeyInCreationV0Setters, +}; +use dpp::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; +use dpp::state_transition::StateTransition; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use dpp::util::hash::hash_double; +use dpp::util::strings::convert_to_homograph_safe_chars; +use dpp::ProtocolError; +use platform_version::version::PlatformVersion; + +use crate::types::{id32, BuiltTransition, KeyInfo}; + +/// Signs a 32-byte digest with the wallet key identified by `key_id`, +/// returning a 65-byte compact recoverable ECDSA signature, or `None` on +/// failure (locked wallet, unknown key). `ASSET_LOCK_KEY_ID` selects the +/// one-time asset-lock key of an identity registration. +pub type SignFn<'a> = &'a (dyn Fn(u32, [u8; 32]) -> Option> + Sync); + +/// Pseudo key id routed to the asset-lock one-time key. +pub const ASSET_LOCK_KEY_ID: u32 = u32::MAX; + +const COMPACT_SIG_SIZE: usize = 65; +const COMPRESSED_PUBKEY_SIZE: usize = 33; + +/// dpp async `Signer` backed by the digest callback. The async surface is +/// signature-only: the callback is invoked synchronously and the builders +/// are driven by `futures::executor::block_on` on the calling thread. +struct CallbackSigner<'a> { + sign_fn: SignFn<'a>, +} + +impl std::fmt::Debug for CallbackSigner<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CallbackSigner") + } +} + +fn sign_digest(sign_fn: SignFn<'_>, key_id: u32, digest: [u8; 32]) -> Result, String> { + let signature = sign_fn(key_id, digest) + .ok_or("signing failed (wallet locked or key unavailable)".to_string())?; + if signature.len() != COMPACT_SIG_SIZE { + return Err(format!( + "unexpected signature size {} (want {COMPACT_SIG_SIZE})", + signature.len() + )); + } + Ok(signature) +} + +#[async_trait::async_trait] +impl Signer for CallbackSigner<'_> { + async fn sign( + &self, + key: &IdentityPublicKey, + data: &[u8], + ) -> Result { + sign_digest(self.sign_fn, key.id(), hash_double(data)) + .map(Into::into) + .map_err(ProtocolError::Generic) + } + + async fn sign_create_witness( + &self, + key: &IdentityPublicKey, + data: &[u8], + ) -> Result { + let signature = self.sign(key, data).await?; + Ok(AddressWitness::P2pkh { signature }) + } + + fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool { + true + } +} + +/// Reconstructs a dpp IdentityPublicKey from the flattened FFI form. +pub fn identity_key_from_info(info: &KeyInfo) -> Result { + Ok(IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: info.id, + purpose: Purpose::try_from(info.purpose) + .map_err(|e| format!("bad key purpose {}: {e}", info.purpose))?, + security_level: SecurityLevel::try_from(info.security_level) + .map_err(|e| format!("bad key security level {}: {e}", info.security_level))?, + contract_bounds: None, + key_type: KeyType::try_from(info.key_type) + .map_err(|e| format!("bad key type {}: {e}", info.key_type))?, + read_only: info.read_only, + data: BinaryData::new(info.data.clone()), + disabled_at: info.disabled_at, + })) +} + +fn load_contract(contract: SystemDataContract) -> Result { + load_system_data_contract(contract, PlatformVersion::latest()) + .map_err(|e| format!("unable to load system data contract: {e}")) +} + +fn built(state_transition: &StateTransition) -> Result { + let bytes = state_transition + .serialize_to_bytes() + .map_err(|e| format!("unable to serialize state transition: {e}"))?; + let hash = sha256::Hash::hash(&bytes).to_byte_array(); + Ok(BuiltTransition { bytes, hash }) +} + +/// Builds and signs a single-document create batch transition from an +/// assembled document. The upstream put-document guards run first: the +/// entropy must rederive the document id (Drive recomputes it during +/// validation, so a mismatch is caught locally instead of after paying a +/// nonce bump), and the properties are sanitized for the document type. +fn build_document_create_transition( + contract: &DataContract, + document_type_name: &str, + document: Document, + identity_contract_nonce: u64, + entropy: [u8; 32], + key: &KeyInfo, + sign_fn: SignFn<'_>, +) -> Result { + use dpp::document::DocumentV0Getters; + + let version = PlatformVersion::latest(); + let document_type = contract + .document_type_for_name(document_type_name) + .map_err(|e| format!("unknown document type {document_type_name}: {e}"))?; + ensure_entropy_matches_document_id( + &contract.id(), + &document.owner_id(), + document_type_name, + &entropy, + document.id(), + ) + .map_err(|e| format!("{document_type_name}: {e}"))?; + let document_type_owned = contract + .document_type_cloned_for_name(document_type_name) + .map_err(|e| format!("unknown document type {document_type_name}: {e}"))?; + let document = prepare_document_for_transition(&document, &document_type_owned); + let identity_key = identity_key_from_info(key)?; + let signer = CallbackSigner { sign_fn }; + let state_transition = futures::executor::block_on( + BatchTransition::new_document_creation_transition_from_document( + document, + document_type, + entropy, + &identity_key, + identity_contract_nonce, + 0, + None, + &signer, + version, + None, + ), + ) + .map_err(|e| format!("unable to build {document_type_name} create transition: {e}"))?; + built(&state_transition) +} + +/// Builds and signs a single-document create batch transition from a +/// property map (documents without an upstream pure builder). +#[allow(clippy::too_many_arguments)] +fn build_document_create( + contract: SystemDataContract, + document_type_name: &str, + owner_id: [u8; 32], + identity_contract_nonce: u64, + properties: BTreeMap, + entropy: [u8; 32], + key: &KeyInfo, + sign_fn: SignFn<'_>, +) -> Result { + let contract = load_contract(contract)?; + let owner_id = Identifier::from(owner_id); + let document_id = + Document::generate_document_id_v0(&contract.id(), &owner_id, document_type_name, &entropy); + let document = Document::V0(DocumentV0 { + id: document_id, + owner_id, + properties, + ..Default::default() + }); + build_document_create_transition( + &contract, + document_type_name, + document, + identity_contract_nonce, + entropy, + key, + sign_fn, + ) +} + +/// The salted domain hash the DPNS preorder blinds the name behind: +/// sha256d(salt || ".dash"). Matches the preimage +/// `build_dpns_preorder_and_domain_documents` hashes into the preorder's +/// `saltedDomainHash` property. +fn salted_domain_hash(salt: [u8; 32], label: &str) -> [u8; 32] { + let normalized = convert_to_homograph_safe_chars(label); + let mut preimage = salt.to_vec(); + preimage.extend((normalized + ".dash").as_bytes()); + hash_double(&preimage) +} + +/// DPNS preorder create. The documents come from the upstream pure builder +/// (dash-platform-queries); the salted domain hash doubles as the document +/// entropy — it is already blinded and unique per (name, salt), so rebuilds +/// of the same registration stay byte-identical. +pub fn build_dpns_preorder( + identity_id: &[u8], + identity_contract_nonce: u64, + label: &str, + preorder_salt: &[u8], + key: &KeyInfo, + sign_fn: SignFn<'_>, +) -> Result { + let owner = id32(identity_id, "identity id")?; + let salt = id32(preorder_salt, "preorder salt")?; + let entropy = salted_domain_hash(salt, label); + let contract = load_contract(SystemDataContract::DPNS)?; + let (preorder, _domain) = build_dpns_preorder_and_domain_documents( + &contract, + Identifier::from(owner), + label, + entropy, + salt, + ) + .map_err(|e| format!("unable to build DPNS preorder document: {e}"))?; + build_document_create_transition( + &contract, + "preorder", + preorder, + identity_contract_nonce, + entropy, + key, + sign_fn, + ) +} + +/// DPNS domain create. The preorder salt is drawn fresh per registration +/// attempt, making it a suitable deterministic document entropy for the +/// paired domain create. The contested-name vote-resolution prefund is +/// computed by rs-dpp from the contested unique index of the DPNS domain +/// type during DocumentCreateTransition::from_document; no explicit +/// handling needed. +#[allow(clippy::too_many_arguments)] +pub fn build_dpns_domain( + identity_id: &[u8], + identity_contract_nonce: u64, + label: &str, + normalized_label: &str, + parent_domain: &str, + preorder_salt: &[u8], + key: &KeyInfo, + sign_fn: SignFn<'_>, +) -> Result { + if convert_to_homograph_safe_chars(label) != normalized_label { + return Err("normalized label does not match label".to_string()); + } + // The upstream builder registers under the "dash" TLD; that is the only + // parent domain that exists. + if parent_domain != "dash" { + return Err(format!("unsupported parent domain {parent_domain:?}")); + } + let owner = id32(identity_id, "identity id")?; + let salt = id32(preorder_salt, "preorder salt")?; + let contract = load_contract(SystemDataContract::DPNS)?; + let (_preorder, domain) = build_dpns_preorder_and_domain_documents( + &contract, + Identifier::from(owner), + label, + salt, + salt, + ) + .map_err(|e| format!("unable to build DPNS domain document: {e}"))?; + build_document_create_transition( + &contract, + "domain", + domain, + identity_contract_nonce, + salt, + key, + sign_fn, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_profile( + identity_id: &[u8], + identity_contract_nonce: u64, + display_name: &str, + public_message: &str, + avatar_url: &str, + avatar_hash: &[u8], + avatar_fingerprint: &[u8], + revision: u64, + existing_document_id: Option<&[u8]>, + entropy: &[u8], + key: &KeyInfo, + sign_fn: SignFn<'_>, +) -> Result { + let owner = id32(identity_id, "identity id")?; + let entropy = id32(entropy, "entropy")?; + if !avatar_hash.is_empty() && avatar_hash.len() != 32 { + return Err("avatar hash must be 32 bytes".to_string()); + } + if !avatar_fingerprint.is_empty() && avatar_fingerprint.len() != 8 { + return Err("avatar fingerprint must be 8 bytes".to_string()); + } + // All profile fields are optional in the DashPay contract; $createdAt / + // $updatedAt are assigned by the chain and never appear in the + // transition. + let mut properties = BTreeMap::new(); + if !display_name.is_empty() { + properties.insert( + "displayName".to_string(), + Value::Text(display_name.to_string()), + ); + } + if !public_message.is_empty() { + properties.insert( + "publicMessage".to_string(), + Value::Text(public_message.to_string()), + ); + } + if !avatar_url.is_empty() { + properties.insert("avatarUrl".to_string(), Value::Text(avatar_url.to_string())); + } + if !avatar_hash.is_empty() { + properties.insert( + "avatarHash".to_string(), + Value::Bytes32(id32(avatar_hash, "avatar hash")?), + ); + } + if !avatar_fingerprint.is_empty() { + properties.insert( + "avatarFingerprint".to_string(), + Value::Bytes(avatar_fingerprint.to_vec()), + ); + } + + let Some(existing_document_id) = existing_document_id else { + if revision != 1 { + return Err("profile create requires revision 1".to_string()); + } + return build_document_create( + SystemDataContract::Dashpay, + "profile", + owner, + identity_contract_nonce, + properties, + entropy, + key, + sign_fn, + ); + }; + + if revision < 2 { + return Err("profile replace requires revision > 1".to_string()); + } + let version = PlatformVersion::latest(); + let contract = load_contract(SystemDataContract::Dashpay)?; + let document_type = contract + .document_type_for_name("profile") + .map_err(|e| format!("unknown document type profile: {e}"))?; + let document = Document::V0(DocumentV0 { + id: Identifier::from(id32(existing_document_id, "existing document id")?), + owner_id: Identifier::from(owner), + properties, + revision: Some(revision), + ..Default::default() + }); + let identity_key = identity_key_from_info(key)?; + let signer = CallbackSigner { sign_fn }; + let state_transition = futures::executor::block_on( + BatchTransition::new_document_replacement_transition_from_document( + document, + document_type, + &identity_key, + identity_contract_nonce, + 0, + None, + &signer, + version, + None, + ), + ) + .map_err(|e| format!("unable to build profile replace transition: {e}"))?; + built(&state_transition) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_contact_request( + identity_id: &[u8], + identity_contract_nonce: u64, + to_user_id: &[u8], + encrypted_public_key: &[u8], + sender_key_index: u32, + recipient_key_index: u32, + account_reference: u32, + encrypted_account_label: &[u8], + entropy: &[u8], + key: &KeyInfo, + sign_fn: SignFn<'_>, +) -> Result { + let owner = id32(identity_id, "identity id")?; + let to_user = id32(to_user_id, "to-user id")?; + let entropy = id32(entropy, "entropy")?; + let contract = load_contract(SystemDataContract::Dashpay)?; + // The upstream builder validates the crypto-material sizes and assembles + // the DIP-15 property map; $createdAt and $createdAtCoreBlockHeight are + // chain-assigned system fields, so they do not enter the transition. + let (document_id, properties) = build_contact_request_document( + &contract, + ContactRequestDocumentParams { + sender_id: Identifier::from(owner), + recipient_id: Identifier::from(to_user), + sender_key_index, + recipient_key_index, + account_reference, + encrypted_public_key: encrypted_public_key.to_vec(), + encrypted_account_label: (!encrypted_account_label.is_empty()) + .then(|| encrypted_account_label.to_vec()), + auto_accept_proof: None, + entropy, + }, + ) + .map_err(|e| format!("unable to build contact request document: {e}"))?; + let document = Document::V0(DocumentV0 { + id: document_id, + owner_id: Identifier::from(owner), + properties, + ..Default::default() + }); + build_document_create_transition( + &contract, + "contactRequest", + document, + identity_contract_nonce, + entropy, + key, + sign_fn, + ) +} + +/// Asset lock proof input for identity registration. +pub enum AssetLockProofInput { + Instant { + /// Serialized asset lock transaction. + transaction: Vec, + /// Serialized islock message. + instant_lock: Vec, + /// Index of the asset-lock OP_RETURN output in tx.vout. + output_index: u32, + }, + Chain { + core_chain_locked_height: u32, + /// txid || vout (LE u32), consensus encoding. + out_point: [u8; 36], + }, +} + +/// A public key to register with a new identity. The private key never +/// crosses; each key proves ownership through the signing callback. +pub struct NewIdentityKey { + pub id: u32, + pub purpose: u8, + pub security_level: u8, + /// Compressed secp256k1 public key (33 bytes). + pub pubkey: Vec, +} + +/// IdentityCreateTransition assembled manually so that both the identity +/// keys and the asset-lock one-time key stay behind the signing callback. +/// Mirrors rs-dpp `try_from_identity_with_signer_and_private_key`: all +/// per-key signatures and the outer asset-lock signature cover the same +/// signable bytes (key signatures, the outer signature and the identity id +/// are all excluded from the signable form). +pub fn build_identity_create( + proof: AssetLockProofInput, + keys: &[NewIdentityKey], + sign_fn: SignFn<'_>, +) -> Result { + if keys.is_empty() { + return Err("no identity keys provided".to_string()); + } + + let asset_lock_proof = match proof { + AssetLockProofInput::Instant { + transaction, + instant_lock, + output_index, + } => { + let transaction = Transaction::consensus_decode(&mut transaction.as_slice()) + .map_err(|e| format!("bad asset lock transaction: {e}"))?; + let instant_lock = InstantLock::consensus_decode(&mut instant_lock.as_slice()) + .map_err(|e| format!("bad instant lock: {e}"))?; + AssetLockProof::Instant(InstantAssetLockProof::new( + instant_lock, + transaction, + output_index, + )) + } + AssetLockProofInput::Chain { + core_chain_locked_height, + out_point, + } => { + let out_point = OutPoint::consensus_decode(&mut out_point.as_slice()) + .map_err(|e| format!("bad asset lock outpoint: {e}"))?; + AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height, + out_point, + }) + } + }; + + // rs-dpp registers Identity::public_keys() (a BTreeMap keyed by id), so + // the keys serialize in ascending id order with no duplicates. + let mut sorted: Vec<&NewIdentityKey> = keys.iter().collect(); + sorted.sort_by_key(|key| key.id); + let mut public_keys = Vec::with_capacity(sorted.len()); + for (i, key) in sorted.iter().enumerate() { + if i > 0 && key.id == sorted[i - 1].id { + return Err(format!("duplicate identity key id {}", key.id)); + } + if key.pubkey.len() != COMPRESSED_PUBKEY_SIZE { + return Err(format!( + "identity key {}: unexpected public key size {}", + key.id, + key.pubkey.len() + )); + } + public_keys.push( + IdentityPublicKeyInCreationV0 { + id: key.id, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::try_from(key.purpose) + .map_err(|e| format!("identity key {}: bad purpose: {e}", key.id))?, + security_level: SecurityLevel::try_from(key.security_level) + .map_err(|e| format!("identity key {}: bad security level: {e}", key.id))?, + contract_bounds: None, + read_only: false, + data: BinaryData::new(key.pubkey.clone()), + signature: Default::default(), + } + .into(), + ); + } + + let identity_id = asset_lock_proof + .create_identifier() + .map_err(|e| format!("unable to derive identity id: {e}"))?; + let mut transition = IdentityCreateTransitionV0 { + public_keys, + asset_lock_proof, + user_fee_increase: 0, + identity_id, + ..Default::default() + }; + + // Every registered key proves ownership by signing the same digest as + // the asset-lock key: the double-SHA256 of the signable bytes. Key + // signatures are excluded from the signable form, so setting them does + // not change it. + let state_transition: StateTransition = transition.clone().into(); + let signable = state_transition + .signable_bytes() + .map_err(|e| format!("unable to compute signable bytes: {e}"))?; + let digest = hash_double(&signable); + for key in transition.public_keys.iter_mut() { + let signature = sign_digest(sign_fn, key.id(), digest) + .map_err(|e| format!("identity key {}: {e}", key.id()))?; + key.set_signature(BinaryData::new(signature)); + } + let mut state_transition: StateTransition = transition.into(); + let signature = sign_digest(sign_fn, ASSET_LOCK_KEY_ID, digest) + .map_err(|e| format!("asset lock key: {e}"))?; + if !state_transition.set_signature(BinaryData::new(signature)) { + return Err("unable to set asset lock signature".to_string()); + } + built(&state_transition) +} diff --git a/packages/rs-platform-cxx/src/types.rs b/packages/rs-platform-cxx/src/types.rs new file mode 100644 index 00000000000..0c0efddb9a0 --- /dev/null +++ b/packages/rs-platform-cxx/src/types.rs @@ -0,0 +1,112 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Plain-Rust result types shared by the core modules. The cxx bridge in +//! `lib.rs` converts these into the flat shared structs C++ sees. + +/// One identity public key, flattened for FFI use. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyInfo { + pub id: u32, + pub purpose: u8, + pub security_level: u8, + pub key_type: u8, + pub read_only: bool, + pub data: Vec, + pub disabled_at: Option, +} + +/// Decoded contested-resource vote state (getContestedResourceVoteState, +/// result type VoteTally with locked and abstaining tallies). +#[derive(Debug, Clone, Default)] +pub struct ContestedVoteState { + pub contest_found: bool, + /// identity -> votes + pub contenders: Vec<([u8; 32], Option)>, + pub abstain_votes: Option, + pub lock_votes: Option, + pub finished: bool, + pub locked: bool, + pub winner: Option<[u8; 32]>, + pub finished_at_time_ms: u64, +} + +/// Decoded identity. +#[derive(Debug, Clone)] +pub struct IdentityInfo { + pub id: [u8; 32], + pub balance: u64, + pub revision: u64, + pub keys: Vec, +} + +/// Decoded DPNS `domain` document fields exposed through this binding. +#[derive(Debug, Clone, Default)] +pub struct DpnsName { + pub label: String, + pub normalized_label: String, + /// normalizedParentDomainName ("dash") + pub parent_domain: String, + pub identity: [u8; 32], + pub document_id: [u8; 32], + pub owner_id: [u8; 32], +} + +/// Decoded DashPay `profile` document. +#[derive(Debug, Clone, Default)] +pub struct Profile { + pub document_id: [u8; 32], + pub owner_id: [u8; 32], + pub display_name: String, + pub public_message: String, + pub avatar_url: String, + pub avatar_hash: Vec, + pub avatar_fingerprint: Vec, + pub created_at: u64, + pub updated_at: u64, + pub revision: u64, +} + +/// Decoded DashPay `contactRequest` document. +#[derive(Debug, Clone, Default)] +pub struct ContactRequest { + pub document_id: [u8; 32], + pub owner_id: [u8; 32], + pub to_user_id: [u8; 32], + pub encrypted_public_key: Vec, + pub sender_key_index: u32, + pub recipient_key_index: u32, + pub account_reference: u32, + pub encrypted_account_label: Vec, + pub core_height_created_at: u32, + pub created_at: u64, +} + +/// The authenticated ResponseMetadata fields of a proved DAPI response. The +/// Tenderdash quorum signature covers all of them (they enter the StateId / +/// CanonicalVote sign bytes), so after `FromProof` verification succeeds the +/// C++ freshness tracker can trust them. +#[derive(Debug, Clone, Default)] +pub struct Meta { + pub height: u64, + pub core_chain_locked_height: u32, + pub time_ms: u64, + pub protocol_version: u32, + pub chain_id: String, +} + +/// A built, signed state transition. +#[derive(Debug, Clone)] +pub struct BuiltTransition { + /// Serialized signed state transition. + pub bytes: Vec, + /// sha256(bytes) - the wait handle for waitForStateTransitionResult. + pub hash: [u8; 32], +} + +pub fn id32(bytes: &[u8], what: &str) -> Result<[u8; 32], String> { + bytes + .try_into() + .map_err(|_| format!("{what} must be 32 bytes, got {}", bytes.len())) +} diff --git a/packages/rs-platform-cxx/src/verify.rs b/packages/rs-platform-cxx/src/verify.rs new file mode 100644 index 00000000000..19f8192729b --- /dev/null +++ b/packages/rs-platform-cxx/src/verify.rs @@ -0,0 +1,307 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Drive proved-response verification for the DAPI queries exposed by this +//! binding, built on drive-proof-verifier's `FromProof`. Each function takes +//! the exact protobuf request the transport sent plus the full protobuf +//! response it received, reconstructs the query from the request, replays the +//! GroveDB proof, and verifies the Tenderdash BLS quorum threshold signature +//! against the keys served by [`crate::provider`]. + +use platform_version::version::PlatformVersion; + +use crate::types::{ContestedVoteState, KeyInfo}; + +// --------------------------------------------------------------------------- +// FromProof-driven verification over (request bytes, response bytes). +// +// The C++ transport hands over the exact protobuf request it sent and the +// full protobuf response it received; drive-proof-verifier reconstructs the +// query from the request, replays the GroveDB proof, and verifies the +// Tenderdash quorum threshold signature against the quorum keys served by +// crate::provider. The returned Meta fields are authenticated by that +// signature. +// --------------------------------------------------------------------------- + +use dapi_grpc::platform::v0::get_documents_request::Version as GetDocumentsVersion; +use dapi_grpc::platform::v0::{ + GetContestedResourceVoteStateRequest, GetContestedResourceVoteStateResponse, + GetDocumentsRequest, GetDocumentsResponse, GetIdentityBalanceRequest, + GetIdentityBalanceResponse, GetIdentityByPublicKeyHashRequest, + GetIdentityByPublicKeyHashResponse, GetIdentityContractNonceRequest, + GetIdentityContractNonceResponse, GetIdentityKeysRequest, GetIdentityKeysResponse, + GetIdentityNonceRequest, GetIdentityNonceResponse, GetIdentityRequest, GetIdentityResponse, + ResponseMetadata, +}; +use dapi_grpc::platform::VersionedGrpcResponse; +use dash_context_provider::ContextProvider as _; +use dash_platform_queries::documents::document_query::verify_documents_response; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; +use dpp::identity::Identity; +use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo as WinnerInfo; +use drive_proof_verifier::types::{ + Contenders, IdentityBalance, IdentityContractNonceFetcher, IdentityNonceFetcher, + IdentityPublicKeys, +}; +use drive_proof_verifier::FromProof; +use prost::Message; + +use crate::decode::identity_info; +use crate::provider::{network, provider}; +use crate::types::{IdentityInfo, Meta}; + +fn decode_message(bytes: &[u8], what: &str) -> Result { + T::decode(bytes).map_err(|e| format!("unable to decode {what}: {e}")) +} + +fn meta_from(mtd: &ResponseMetadata) -> Meta { + Meta { + height: mtd.height, + core_chain_locked_height: mtd.core_chain_locked_height, + time_ms: mtd.time_ms, + protocol_version: mtd.protocol_version, + chain_id: mtd.chain_id.clone(), + } +} + +/// The platform version the response claims to be produced under, falling +/// back to the crate's latest known version for responses from a newer +/// server. The claimed version is authenticated after the fact: it enters +/// the signed StateId, so a lie fails the quorum signature check. +fn response_version(response: &R) -> &'static PlatformVersion +where + ::Error: std::fmt::Display, +{ + response + .metadata() + .ok() + .and_then(|mtd| PlatformVersion::get(mtd.protocol_version).ok()) + .unwrap_or_else(PlatformVersion::latest) +} + +pub fn verify_get_identity_nonce( + request: &[u8], + response: &[u8], +) -> Result<(Option, Meta), String> { + let request: GetIdentityNonceRequest = decode_message(request, "GetIdentityNonceRequest")?; + let response: GetIdentityNonceResponse = decode_message(response, "GetIdentityNonceResponse")?; + let version = response_version(&response); + let (nonce, mtd, _) = IdentityNonceFetcher::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("identity nonce proof verification failed: {e}"))?; + Ok((nonce.map(|fetcher| fetcher.0), meta_from(&mtd))) +} + +pub fn verify_get_identity_contract_nonce( + request: &[u8], + response: &[u8], +) -> Result<(Option, Meta), String> { + let request: GetIdentityContractNonceRequest = + decode_message(request, "GetIdentityContractNonceRequest")?; + let response: GetIdentityContractNonceResponse = + decode_message(response, "GetIdentityContractNonceResponse")?; + let version = response_version(&response); + let (nonce, mtd, _) = IdentityContractNonceFetcher::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("identity contract nonce proof verification failed: {e}"))?; + Ok((nonce.map(|fetcher| fetcher.0), meta_from(&mtd))) +} + +/// getIdentity: one proof covering balance, revision and keys +/// (Drive::verify_full_identity_by_identity_id). `None` = proven absent. +pub fn verify_get_identity( + request: &[u8], + response: &[u8], +) -> Result<(Option, Meta), String> { + let request: GetIdentityRequest = decode_message(request, "GetIdentityRequest")?; + let response: GetIdentityResponse = decode_message(response, "GetIdentityResponse")?; + let version = response_version(&response); + let (identity, mtd, _) = + >::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("identity proof verification failed: {e}"))?; + Ok((identity.as_ref().map(identity_info), meta_from(&mtd))) +} + +/// getIdentityByPublicKeyHash: one proof resolving the unique key hash to +/// the full identity. `None` = no identity registered that key hash. +pub fn verify_get_identity_by_pubkey_hash( + request: &[u8], + response: &[u8], +) -> Result<(Option, Meta), String> { + let request: GetIdentityByPublicKeyHashRequest = + decode_message(request, "GetIdentityByPublicKeyHashRequest")?; + let response: GetIdentityByPublicKeyHashResponse = + decode_message(response, "GetIdentityByPublicKeyHashResponse")?; + let version = response_version(&response); + let (identity, mtd, _) = + >::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("identity-by-public-key-hash proof verification failed: {e}"))?; + Ok((identity.as_ref().map(identity_info), meta_from(&mtd))) +} + +/// getIdentityBalance. Not bridged to C++ (the client fetches balances via +/// the full identity); exercised by the vector tests to pin the shared +/// grovedb + quorum-signature pipeline against the fixture corpus. +pub fn verify_get_identity_balance( + request: &[u8], + response: &[u8], +) -> Result<(Option, Meta), String> { + let request: GetIdentityBalanceRequest = decode_message(request, "GetIdentityBalanceRequest")?; + let response: GetIdentityBalanceResponse = + decode_message(response, "GetIdentityBalanceResponse")?; + let version = response_version(&response); + let (balance, mtd, _) = IdentityBalance::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("identity balance proof verification failed: {e}"))?; + Ok((balance, meta_from(&mtd))) +} + +/// getIdentityKeys (all keys). Not bridged to C++ (see +/// `verify_get_identity_balance`). +pub fn verify_get_identity_keys( + request: &[u8], + response: &[u8], +) -> Result<(Option>, Meta), String> { + let request: GetIdentityKeysRequest = decode_message(request, "GetIdentityKeysRequest")?; + let response: GetIdentityKeysResponse = decode_message(response, "GetIdentityKeysResponse")?; + let version = response_version(&response); + let (keys, mtd, _) = IdentityPublicKeys::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("identity keys proof verification failed: {e}"))?; + let keys = keys.map(|keys| { + keys.values() + .flatten() + .map(crate::decode::key_info) + .collect::>() + }); + Ok((keys, meta_from(&mtd))) +} + +/// getDocuments: reconstructs the query from the request +/// (DocumentQuery::try_from_request), verifies the proof, and returns the +/// matched documents re-serialized in platform form (the input to the +/// decode_* functions). Absence (no matching documents) is an empty vector. +pub fn verify_get_documents( + request: &[u8], + response: &[u8], +) -> Result<(Vec>, Meta), String> { + let request: GetDocumentsRequest = decode_message(request, "GetDocumentsRequest")?; + let response: GetDocumentsResponse = decode_message(response, "GetDocumentsResponse")?; + let version = response_version(&response); + + let (contract_id_bytes, document_type_name) = match &request.version { + Some(GetDocumentsVersion::V0(v0)) => { + (v0.data_contract_id.clone(), v0.document_type.clone()) + } + Some(GetDocumentsVersion::V1(v1)) => { + (v1.data_contract_id.clone(), v1.document_type.clone()) + } + None => return Err("GetDocumentsRequest has no version set".to_string()), + }; + let contract_id = crate::types::id32(&contract_id_bytes, "contract id")?; + let contract = provider() + .get_data_contract(&contract_id.into(), version) + .map_err(|e| format!("unable to resolve data contract: {e}"))? + .ok_or("document queries support only the pinned system contracts")?; + + let (documents, mtd, _) = verify_documents_response( + request, + std::sync::Arc::clone(&contract), + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("document proof verification failed: {e}"))?; + + let document_type = contract + .document_type_for_name(&document_type_name) + .map_err(|e| format!("unknown document type {document_type_name}: {e}"))?; + let mut serialized = Vec::new(); + for document in documents.into_iter().flatten().filter_map(|(_, doc)| doc) { + serialized.push( + document + .serialize(document_type, &contract, version) + .map_err(|e| format!("unable to re-serialize verified document: {e}"))?, + ); + } + Ok((serialized, meta_from(&mtd))) +} + +/// getContestedResourceVoteState (VoteTally result type). The query shape — +/// contract, document type, index values, tally options, count — is +/// reconstructed from the request itself. +pub fn verify_get_contested_vote_state( + request: &[u8], + response: &[u8], +) -> Result<(ContestedVoteState, Meta), String> { + let request: GetContestedResourceVoteStateRequest = + decode_message(request, "GetContestedResourceVoteStateRequest")?; + let response: GetContestedResourceVoteStateResponse = + decode_message(response, "GetContestedResourceVoteStateResponse")?; + let version = response_version(&response); + let (contenders, mtd, _) = Contenders::maybe_from_proof_with_metadata( + request, + response, + network()?, + version, + provider(), + ) + .map_err(|e| format!("contested vote state proof verification failed: {e}"))?; + + let mut state = ContestedVoteState::default(); + if let Some(contenders) = contenders { + state.contest_found = true; + state.contenders = contenders + .contenders + .iter() + .map(|(id, contender)| (id.to_buffer(), contender.vote_tally())) + .collect(); + state.abstain_votes = contenders.abstain_vote_tally; + state.lock_votes = contenders.lock_vote_tally; + if let Some((winner_info, finalization_block)) = contenders.winner { + state.finished = true; + state.finished_at_time_ms = finalization_block.time_ms; + match winner_info { + WinnerInfo::WonByIdentity(id) => state.winner = Some(id.to_buffer()), + WinnerInfo::Locked => state.locked = true, + WinnerInfo::NoWinner => {} + } + } + } + Ok((state, meta_from(&mtd))) +} diff --git a/packages/rs-platform-cxx/standalone/.gitignore b/packages/rs-platform-cxx/standalone/.gitignore new file mode 100644 index 00000000000..b83d22266ac --- /dev/null +++ b/packages/rs-platform-cxx/standalone/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/packages/rs-platform-cxx/standalone/Cargo.lock b/packages/rs-platform-cxx/standalone/Cargo.lock new file mode 100644 index 00000000000..2d58687a1df --- /dev/null +++ b/packages/rs-platform-cxx/standalone/Cargo.lock @@ -0,0 +1,3960 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue 0.0.18", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6712f9c6fd6785b3b270884e57c441c403dc5d7e19ca45368c97c7a1de3000ec" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "blsful" +version = "3.0.0" +source = "git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900#0c34a7a488a0bd1c9a9a2196e793b303ad35c900" +dependencies = [ + "anyhow", + "blstrs_plus", + "hex", + "hkdf", + "merlin", + "pairing", + "rand", + "rand_chacha", + "rand_core", + "serde", + "serde_bare", + "sha2", + "sha3", + "subtle", + "thiserror 2.0.20", + "uint-zigzag", + "vsss-rs", + "zeroize", +] + +[[package]] +name = "blst" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62dc83a094a71d43eeadd254b1ec2d24cb6a0bb6cadce00df51f0db594711a32" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "blstrs_plus" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a16dd4b0d6b4538e1fa0388843acb186363082713a8fc8416d802a04d013818" +dependencies = [ + "arrayref", + "blst", + "elliptic-curve", + "ff", + "group", + "pairing", + "rand_core", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbindgen" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20" +dependencies = [ + "clap", + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.9", + "rand_core", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.9", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cxx" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash 0.2.0", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dapi-grpc" +version = "4.2.0-dev.1" +dependencies = [ + "dash-platform-macros", + "futures-core", + "getrandom 0.2.17", + "platform-version", + "prost", + "tenderdash-proto", + "tonic", + "tonic-prost", + "tonic-prost-build", +] + +[[package]] +name = "dash-async" +version = "4.2.0-dev.1" +dependencies = [ + "futures", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "dash-context-provider" +version = "4.2.0-dev.1" +dependencies = [ + "dash-async", + "dpp", + "drive", + "hex", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "dash-network" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb#173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" +dependencies = [ + "bincode", + "bincode_derive", + "cbindgen", + "serde", +] + +[[package]] +name = "dash-platform-cxx" +version = "4.2.0-dev.1" +dependencies = [ + "async-trait", + "cxx", + "cxx-build", + "dapi-grpc", + "dash-context-provider", + "dash-platform-queries", + "dpp", + "drive-proof-verifier", + "futures", + "hex", + "platform-version", + "prost", +] + +[[package]] +name = "dash-platform-cxx-standalone" +version = "0.0.0" +dependencies = [ + "dash-platform-cxx", +] + +[[package]] +name = "dash-platform-macros" +version = "4.2.0-dev.1" +dependencies = [ + "heck", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dash-platform-queries" +version = "4.2.0-dev.1" +dependencies = [ + "ciborium", + "dapi-grpc", + "dash-context-provider", + "dash-platform-macros", + "dpp", + "drive", + "drive-proof-verifier", + "hex", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "dashcore" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb#173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" +dependencies = [ + "anyhow", + "base64-compat", + "bech32 0.9.1", + "bincode", + "bincode_derive", + "bitvec", + "blake3", + "blsful", + "dash-network", + "dashcore-private", + "dashcore_hashes", + "ed25519-dalek", + "hex", + "hex_lit", + "rustversion", + "secp256k1", + "serde", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "dashcore-private" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb#173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" + +[[package]] +name = "dashcore_hashes" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb#173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" +dependencies = [ + "bincode", + "dashcore-private", + "serde", +] + +[[package]] +name = "dashpay-contract" +version = "4.2.0-dev.1" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "data-contracts" +version = "4.2.0-dev.1" +dependencies = [ + "dashpay-contract", + "dpns-contract", + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.20", + "token-history-contract", + "withdrawals-contract", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dpns-contract" +version = "4.2.0-dev.1" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "dpp" +version = "4.2.0-dev.1" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bech32 0.11.1", + "bincode", + "bs58", + "byteorder", + "chrono", + "dashcore", + "data-contracts", + "derive_more 1.0.0", + "dpp-json-convertible-derive", + "env_logger", + "getrandom 0.2.17", + "hex", + "indexmap", + "integer-encoding", + "itertools 0.13.0", + "lazy_static", + "nohash-hasher", + "num_enum 0.7.6", + "once_cell", + "platform-serialization", + "platform-serialization-derive", + "platform-value", + "platform-version", + "platform-versioning", + "rand", + "regex", + "serde", + "serde_json", + "serde_repr", + "sha2", + "strum", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "dpp-json-convertible-derive" +version = "4.2.0-dev.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "drive" +version = "4.2.0-dev.1" +dependencies = [ + "bincode", + "byteorder", + "derive_more 1.0.0", + "dpp", + "grovedb", + "grovedb-costs", + "grovedb-epoch-based-storage-flags", + "grovedb-path", + "grovedb-version", + "hex", + "indexmap", + "integer-encoding", + "nohash-hasher", + "platform-version", + "sqlparser", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "drive-proof-verifier" +version = "4.2.0-dev.1" +dependencies = [ + "bincode", + "dapi-grpc", + "dash-context-provider", + "derive_more 1.0.0", + "dpp", + "drive", + "hex", + "indexmap", + "platform-serialization", + "tenderdash-abci", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "ed" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c8d6ea916fadcd87e3d1ff4802b696d717c83519b47e76f267ab77e536dd5a" +dependencies = [ + "ed-derive", + "thiserror 1.0.69", +] + +[[package]] +name = "ed-derive" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a91d774f4b861acaa791bc6165e66d72d3a5d1aa85fc8c0956f5580f863161" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.9", + "group", + "hkdf", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "elliptic-curve-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" +dependencies = [ + "elliptic-curve", + "heapless", + "hex", + "multiexp", + "serde", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +dependencies = [ + "rustversion", + "serde_core", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand", + "rand_core", + "rand_xorshift", + "subtle", +] + +[[package]] +name = "grovedb" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "bincode_derive", + "blake3", + "grovedb-bulk-append-tree", + "grovedb-costs", + "grovedb-dense-fixed-sized-merkle-tree", + "grovedb-element", + "grovedb-merk", + "grovedb-merkle-mountain-range", + "grovedb-path", + "grovedb-query", + "grovedb-version", + "hex", + "indexmap", + "integer-encoding", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-bulk-append-tree" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "blake3", + "grovedb-costs", + "grovedb-dense-fixed-sized-merkle-tree", + "grovedb-merkle-mountain-range", + "grovedb-query", + "hex", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-costs" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "integer-encoding", + "intmap", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-dense-fixed-sized-merkle-tree" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "blake3", + "grovedb-costs", + "grovedb-query", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-element" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "bincode_derive", + "grovedb-path", + "grovedb-version", + "hex", + "integer-encoding", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-epoch-based-storage-flags" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "grovedb-costs", + "hex", + "integer-encoding", + "intmap", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-merk" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "bincode_derive", + "blake3", + "byteorder", + "ed", + "grovedb-costs", + "grovedb-element", + "grovedb-path", + "grovedb-query", + "grovedb-version", + "grovedb-visualize", + "hex", + "indexmap", + "integer-encoding", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-merkle-mountain-range" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "blake3", + "grovedb-costs", +] + +[[package]] +name = "grovedb-path" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "hex", +] + +[[package]] +name = "grovedb-query" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "bincode", + "byteorder", + "ed", + "hex", + "indexmap", + "integer-encoding", + "thiserror 2.0.20", +] + +[[package]] +name = "grovedb-version" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "thiserror 2.0.20", + "versioned-feature-core", +] + +[[package]] +name = "grovedb-visualize" +version = "5.0.1" +source = "git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9#a2791bbdca756d6a6113024aec48f09f7a33faa9" +dependencies = [ + "hex", + "itertools 0.14.0", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "integer-encoding" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" + +[[package]] +name = "intmap" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e611826a1868311677fdcdfbec9e8621d104c732d080f546a854530232f0ee" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lhash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744a4c881f502e98c2241d2e5f50040ac73b30194d64452bb6260393b53f0dc9" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiexp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" +dependencies = [ + "ff", + "group", + "rand_core", + "rustversion", + "std-shims", + "zeroize", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "rand", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "rand", + "serde", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive 0.7.6", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "platform-serialization" +version = "4.2.0-dev.1" +dependencies = [ + "bincode", + "platform-version", +] + +[[package]] +name = "platform-serialization-derive" +version = "4.2.0-dev.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "virtue 0.0.17", +] + +[[package]] +name = "platform-value" +version = "4.2.0-dev.1" +dependencies = [ + "base64 0.22.1", + "bincode", + "bs58", + "ciborium", + "hex", + "indexmap", + "platform-serialization", + "platform-version", + "rand", + "serde", + "serde_json", + "thiserror 2.0.20", + "treediff", +] + +[[package]] +name = "platform-version" +version = "4.2.0-dev.1" +dependencies = [ + "bincode", + "grovedb-version", + "thiserror 2.0.20", + "versioned-feature-core", +] + +[[package]] +name = "platform-versioning" +version = "4.2.0-dev.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.1", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bare" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55386eed0f1ae957b091dc2ca8122f287b60c79c774cbe3d5f2b69fded660" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlparser" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0272b7bb0a225320170c99901b4b5fb3a4384e255a7f2cc228f61e2ba3893e75" +dependencies = [ + "log", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "std-shims" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" +dependencies = [ + "hashbrown 0.16.1", + "rustversion", + "spin", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tenderdash-abci" +version = "1.5.1" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.1#fae47294618125fa9d69150e8a7a5b607af6867c" +dependencies = [ + "bytes", + "hex", + "lhash", + "semver", + "tenderdash-proto", + "thiserror 2.0.20", + "tracing", + "url", +] + +[[package]] +name = "tenderdash-proto" +version = "1.5.1" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.1#fae47294618125fa9d69150e8a7a5b607af6867c" +dependencies = [ + "bytes", + "chrono", + "derive_more 2.1.1", + "num-derive", + "num-traits", + "prost", + "subtle-encoding", + "tenderdash-proto-compiler", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "tenderdash-proto-compiler" +version = "1.5.1" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.1#fae47294618125fa9d69150e8a7a5b607af6867c" +dependencies = [ + "fs_extra", + "prost-build", + "regex", + "tempfile", + "ureq", + "walkdir", + "zip", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "token-history-contract" +version = "4.2.0-dev.1" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "treediff" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ce481b2b7c2534fe7b5242cccebf37f9084392665c6a3783c414a1bada5432" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint-zigzag" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61faa33dc26b2851a37da5390a1a4cac015887b1e97ecd77ce7b4f987431de9f" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64 0.23.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "versioned-feature-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898c0ad500fdb1914df465a2c729fce33646ef65dfbbbd16a6d8050e0d2404df" + +[[package]] +name = "virtue" +version = "0.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7302ac74a033bf17b6e609ceec0f891ca9200d502d31f02dc7908d3d98767c9d" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "vsss-rs" +version = "5.1.0" +source = "git+https://github.com/dashpay/vsss-rs?branch=main#668f1406bf25a4b9a95cd97c9069f7a1632897c3" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "elliptic-curve-tools", + "generic-array 1.4.5", + "hex", + "num", + "rand_core", + "serde", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "withdrawals-contract" +version = "4.2.0-dev.1" +dependencies = [ + "num_enum 0.5.11", + "platform-value", + "platform-version", + "serde", + "serde_json", + "serde_repr", + "thiserror 2.0.20", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/packages/rs-platform-cxx/standalone/Cargo.toml b/packages/rs-platform-cxx/standalone/Cargo.toml new file mode 100644 index 00000000000..d4ed8ef9ecc --- /dev/null +++ b/packages/rs-platform-cxx/standalone/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dash-platform-cxx-standalone" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[lib] +name = "dash_platform_cxx_bundle" +crate-type = ["staticlib"] + +[dependencies] +dash-platform-cxx = { path = ".." } + +[patch."https://github.com/dashpay/versioned-feature-core"] +versioned-feature-core = "1.0.0" diff --git a/packages/rs-platform-cxx/standalone/src/lib.rs b/packages/rs-platform-cxx/standalone/src/lib.rs new file mode 100644 index 00000000000..64b103ed15e --- /dev/null +++ b/packages/rs-platform-cxx/standalone/src/lib.rs @@ -0,0 +1 @@ +pub use dash_platform_cxx::*; diff --git a/packages/rs-platform-cxx/test-cxx-link.sh b/packages/rs-platform-cxx/test-cxx-link.sh new file mode 100755 index 00000000000..6f6b504778e --- /dev/null +++ b/packages/rs-platform-cxx/test-cxx-link.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +workspace_dir="$(cd "${package_dir}/../.." && pwd)" +target_dir="${CARGO_TARGET_DIR:-${workspace_dir}/target/platform-cxx-standalone}" +cxx="${CXX:-c++}" +manifest="${package_dir}/standalone/Cargo.toml" + +CARGO_TARGET_DIR="${target_dir}" cargo build --manifest-path "${manifest}" --locked + +stage_dir="$(mktemp -d "${TMPDIR:-/tmp}/dash-platform-cxx.XXXXXX")" +trap 'rm -rf "${stage_dir}"' EXIT + +CARGO_PROFILE=debug CARGO_TARGET_DIR="${target_dir}" \ + "${package_dir}/install.sh" "${stage_dir}" + +system_libs=(-lpthread -lm) +case "$(uname -s)" in + Darwin) system_libs+=(-framework CoreFoundation) ;; + Linux) system_libs+=(-ldl) ;; +esac + +"${cxx}" -std=c++20 -I"${stage_dir}/include" \ + "${package_dir}/tests/cxx_smoke.cc" \ + "${stage_dir}/lib/libdash_platform_cxx.a" \ + "${system_libs[@]}" -o "${stage_dir}/cxx_smoke" +"${stage_dir}/cxx_smoke" diff --git a/packages/rs-platform-cxx/test_data/dpp_identity_vectors.json b/packages/rs-platform-cxx/test_data/dpp_identity_vectors.json new file mode 100644 index 00000000000..b4e6cb3c4ce --- /dev/null +++ b/packages/rs-platform-cxx/test_data/dpp_identity_vectors.json @@ -0,0 +1,52 @@ +{ + "platform_repo_tag": "v4.0.0", + "protocol_version": 12, + "identity": { + "serialized_hex": "00777777777777777777777777777777777777777777777777777777777777777703000000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa0001000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f270002000201030100a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687bfd000000012a05f20003", + "id": "7777777777777777777777777777777777777777777777777777777777777777", + "balance": 5000000000, + "revision": 3, + "public_keys": [ + { + "id": 0, + "purpose": 0, + "security_level": 0, + "key_type": 0, + "read_only": false, + "data": "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa", + "disabled_at": null + }, + { + "id": 1, + "purpose": 0, + "security_level": 2, + "key_type": 0, + "read_only": false, + "data": "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27", + "disabled_at": null + }, + { + "id": 2, + "purpose": 1, + "security_level": 3, + "key_type": 0, + "read_only": false, + "data": "035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c30", + "disabled_at": 1700000000123 + } + ], + "bounded_key_contract_id": "a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc" + }, + "identity_public_key": { + "serialized_hex": "000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2700", + "fields": { + "id": 1, + "purpose": 0, + "security_level": 2, + "key_type": 0, + "read_only": false, + "data": "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27", + "disabled_at": null + } + } +} diff --git a/packages/rs-platform-cxx/test_data/dpp_st_vectors.json b/packages/rs-platform-cxx/test_data/dpp_st_vectors.json new file mode 100644 index 00000000000..3b7a9859420 --- /dev/null +++ b/packages/rs-platform-cxx/test_data/dpp_st_vectors.json @@ -0,0 +1,233 @@ +{ + "platform_repo_tag": "v4.0.0", + "protocol_version": 12, + "signature_scheme": "double-SHA256 of signable bytes; 65-byte compact recoverable ECDSA; header byte = 27 + recovery_id + 4 (compressed)", + "keys": { + "master": { + "id": 0, + "private_key_hex": "1111111111111111111111111111111111111111111111111111111111111111", + "public_key_hex": "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" + }, + "high": { + "id": 1, + "private_key_hex": "2222222222222222222222222222222222222222222222222222222222222222", + "public_key_hex": "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27" + }, + "asset_lock": { + "private_key_hex": "3333333333333333333333333333333333333333333333333333333333333333", + "public_key_hex": "023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1" + } + }, + "identity_create_instant": { + "signable_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2700c60101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000004ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf9bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc620300080001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0100000000ffffffff01a086010000000000016a00000000240101a0860100000000001976a914999999999999999999999999999999999999999988ac0000", + "digest_hex": "65e4f7fd5b7aed2f0eff726970885384934e992b069f69a691e2ca719e9bccb0", + "serialized_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa411f9d83c3c0beb114a82d086a738e3aa5b13a0e70b8add2c78ac3cb90e592bdc4ba299caa0e78460bd08c54b7fb8d7e78879907bd0fc54fb6bfcc69a540ea0de84d000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27411ffc748e31ff48cbcfc2fa304bee54fdb8eaf673d5ab2da8ceb48f8d57dbc83def2e6d9970e108dadf18567d26ecbea8ac457768dc92dc1bdefdf52faac9f7988200c60101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000004ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf9bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc620300080001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0100000000ffffffff01a086010000000000016a00000000240101a0860100000000001976a914999999999999999999999999999999999999999988ac0000411f705a1b57222944f2ce9ea0d728edbc1312f244e326236946336c59e0f8b180ad732d88514d33de9a6f3687a789ae44649835c9f6c55a1e1bbcae6efc7f899b0c15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "transaction_hex": "0300080001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0100000000ffffffff01a086010000000000016a00000000240101a0860100000000001976a914999999999999999999999999999999999999999988ac", + "instant_lock_hex": "0101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000004ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf9bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_index": 0, + "out_point_hex": "4ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf900000000", + "identity_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327" + }, + "identity_create_chain": { + "signable_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2701fc00100590204ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf90000", + "digest_hex": "0a0a332c8bb7cb633707f8aa33ceffd667484e0fcf9d78f8025d3478f7c3b324", + "serialized_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa41200a0b660530dfce50a0b1638ec274f658d662c1755105cfcae5ad31f92af5b0622dee03deefd30f0fa679d729bc89b138e402fc72ad9eee102dafcf2c580ef218000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27411fcd6fc90f5a63a47a7dd2fd037a08570298c9aba925d2bbb2392f6a6f52cc109f23de5102d4b3d2e471a75c875f46dcf0d9ad5be3e6215c014150c77b0b8db62a01fc00100590204ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf900004120d275a61395575038d64f674b8fff1199abef5aae24ca7e560103a67cf71ea021080b97f7c4a7eea3fbe5045695404a766221556f283dcacc08b49beb9568a75915eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "core_chain_locked_height": 1050000, + "out_point_hex": "4ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf900000000", + "identity_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327" + }, + "dpns_preorder": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001fae3f9bb1a28d38c18c65e78ae91730c667f388911988dce5c695f39cba97b1002087072656f72646572e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18011073616c746564446f6d61696e486173680c2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc180000", + "digest_hex": "64ad2dc780ce0718457c2f3ae9b3a6c6c49605ad71ea977e7746c755108fd24b", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001fae3f9bb1a28d38c18c65e78ae91730c667f388911988dce5c695f39cba97b1002087072656f72646572e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18011073616c746564446f6d61696e486173680c2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc1800000141209dcb265b869db7435ee79271ae2ded9fbda2c13478b2c65ca1b087c37226df0716917a41b8233a2b1982518e2d127bb9f2c654cfdb52e902233d2b4c85e8f552", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 2, + "entropy_hex": "2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18", + "salt_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "label": "Alice", + "normalized_label": "a11ce", + "salted_domain_hash_hex": "2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18", + "document_id_hex": "fae3f9bb1a28d38c18c65e78ae91730c667f388911988dce5c695f39cba97b10", + "signature_public_key_id": 1 + }, + "dpns_domain_contested": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87fb123406646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c1205416c6963650f6e6f726d616c697a65644c6162656c120561313163651a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e7313000112706172656e744e616d65416e644c6162656cfd00000004a817c80000", + "digest_hex": "cdddcc3a001075acb4a13546f4d5404df42ae5ee857f0641c8199ac64bd3a90f", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87fb123406646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c1205416c6963650f6e6f726d616c697a65644c6162656c120561313163651a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e7313000112706172656e744e616d65416e644c6162656cfd00000004a817c800000141200307c3087cacd3b3aecbbefae2191c53f02e8b6b58efff84661df8a8ff674eff213f285ddd0f5030e9457d918a925bb1b6d2e3305605508ed26ec6e2fc17acf2", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 4660, + "entropy_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "salt_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "label": "Alice", + "normalized_label": "a11ce", + "document_id_hex": "8e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87", + "signature_public_key_id": 1, + "contested": true, + "prefunded_voting_balance_credits": 20000000000 + }, + "dpns_domain": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba870506646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c12097175616e74756d34320f6e6f726d616c697a65644c6162656c12097175616e74756d34321a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e7313000000", + "digest_hex": "10b93c345995e3fca59bfbb45e6ecc0a794b50f0b9f42de9002d0f935c32a896", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba870506646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c12097175616e74756d34320f6e6f726d616c697a65644c6162656c12097175616e74756d34321a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e731300000001411f63a478bc0626c371ee445ba205244227b734d540a89278ca8cedbe9e48bd88bc215e9871ce443c04d1dacc91c080de6151da51be58225b908e59d33dc1e8c038", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 5, + "entropy_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "salt_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "label": "quantum42", + "normalized_label": "quantum42", + "document_id_hex": "8e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87", + "signature_public_key_id": 1, + "contested": false + }, + "dashpay_profile_create": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6030770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00e540d547ecac78cac1efd88fbced3d23aa8ae638ff1accdaa8a308337983eb09051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120e68656c6c6f20706c6174666f726d0000", + "digest_hex": "9e375dc31f1323adc4cc2e1de0eb5f8e0c6a26d26288b6e74f3b9cd68ea7afe0", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6030770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00e540d547ecac78cac1efd88fbced3d23aa8ae638ff1accdaa8a308337983eb09051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120e68656c6c6f20706c6174666f726d000001411fd491cd71660321908cb4b911d103ec22252cb60f70f073f0d0468dce50ef31aa0de158d69459d3fdb4e177471ed19857cd5f504e2c8becd8fd6f8b57dd8dc455", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 3, + "entropy_hex": "e540d547ecac78cac1efd88fbced3d23aa8ae638ff1accdaa8a308337983eb09", + "document_id_hex": "a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6", + "signature_public_key_id": 1, + "display_name": "Alice в Wonderland", + "public_message": "hello platform", + "avatar_url": "https://example.com/a.png", + "avatar_hash_hex": "8888888888888888888888888888888888888888888888888888888888888888", + "avatar_fingerprint_hex": "9999999999999999" + }, + "dashpay_profile_replace": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100010001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6040770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0002051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120f75706461746564206d65737361676500", + "digest_hex": "f3361516f52e272dfc6fdc75ea304f9c5035cf985550730a3704694cc0a755d1", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100010001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6040770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0002051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120f75706461746564206d65737361676500014120fd2ec82c57e630ce2854a2ec05a14f36d186cb030707bd725b1ae6d79ce7a6194fa8edfba509bbc6f4f4a99a77270a34ec8c345234019078b793a4e5fe9cd33e", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 4, + "revision": 2, + "document_id_hex": "a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6", + "signature_public_key_id": 1, + "public_message": "updated message" + }, + "dashpay_contact_request": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327010000000148880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd060e636f6e7461637452657175657374a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00fda853b87e9145263b6fcb17b4d8545a9462b4daf95a86194b5e8b5a3f74a22406106163636f756e745265666572656e636504fc0badc0de15656e637279707465644163636f756e744c6162656c0a30cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd12656e637279707465645075626c69634b65790a60abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab11726563697069656e744b6579496e64657804030e73656e6465724b6579496e646578040208746f55736572496410eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000", + "digest_hex": "46002d99e2fcdf47cddc3e3173b9ba9256af84e84a4b6e007b2ba4db848cab44", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327010000000148880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd060e636f6e7461637452657175657374a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00fda853b87e9145263b6fcb17b4d8545a9462b4daf95a86194b5e8b5a3f74a22406106163636f756e745265666572656e636504fc0badc0de15656e637279707465644163636f756e744c6162656c0a30cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd12656e637279707465645075626c69634b65790a60abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab11726563697069656e744b6579496e64657804030e73656e6465724b6579496e646578040208746f55736572496410eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000001411fcbc9c34bb69e53b1a319499aada671d480cb8db8e33afd27a633ee21b98dd1c87c8d27c8e2a878edcee85d92fa614bd8c9c3724f86854971a5dadfa31d9e7c2e", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 6, + "entropy_hex": "fda853b87e9145263b6fcb17b4d8545a9462b4daf95a86194b5e8b5a3f74a224", + "document_id_hex": "48880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd", + "signature_public_key_id": 1, + "to_user_id_hex": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "encrypted_public_key_hex": "abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab", + "sender_key_index": 2, + "recipient_key_index": 3, + "account_reference": 195936478, + "encrypted_account_label_hex": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + }, + "contested_labels": [ + { + "label": "alice", + "normalized": "a11ce", + "contested": true + }, + { + "label": "a11ce", + "normalized": "a11ce", + "contested": true + }, + { + "label": "bob", + "normalized": "b0b", + "contested": true + }, + { + "label": "b0b", + "normalized": "b0b", + "contested": true + }, + { + "label": "ab", + "normalized": "ab", + "contested": false + }, + { + "label": "abc", + "normalized": "abc", + "contested": true + }, + { + "label": "x2y", + "normalized": "x2y", + "contested": false + }, + { + "label": "up", + "normalized": "up", + "contested": false + }, + { + "label": "-ab-", + "normalized": "-ab-", + "contested": true + }, + { + "label": "aaaaaaaaaaaaaaaaaaa", + "normalized": "aaaaaaaaaaaaaaaaaaa", + "contested": true + }, + { + "label": "aaaaaaaaaaaaaaaaaaaa", + "normalized": "aaaaaaaaaaaaaaaaaaaa", + "contested": false + }, + { + "label": "quantum42", + "normalized": "quantum42", + "contested": false + }, + { + "label": "dash", + "normalized": "dash", + "contested": true + }, + { + "label": "test-name", + "normalized": "test-name", + "contested": true + }, + { + "label": "name2", + "normalized": "name2", + "contested": false + } + ], + "stored_documents": { + "domain": { + "serialized_hex": "028e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba8715eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327000100070000018bcfe568000000018bcfe568640000018bcfe568c800097175616e74756d3432097175616e74756d3432010464617368046461736800210115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100", + "id_hex": "8e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "label": "quantum42", + "normalized_label": "quantum42" + }, + "profile": { + "serialized_hex": "02a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d615eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270200030000018bcfe568000000018bcfe5692c011968747470733a2f2f6578616d706c652e636f6d2f612e706e67018888888888888888888888888888888888888888888888888888888888888888019999999999999999010f75706461746564206d6573736167650113416c69636520d0b220576f6e6465726c616e64", + "id_hex": "a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "revision": 2, + "display_name": "Alice в Wonderland", + "public_message": "updated message", + "avatar_url": "https://example.com/a.png", + "created_at": 1700000000000, + "updated_at": 1700000000300 + }, + "contact": { + "serialized_hex": "0248880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32700410000018bcfe56990001e8480eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeabababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab00000000000000020000000000000003000000000badc0de0130cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd00", + "id_hex": "48880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "to_user_id_hex": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "sender_key_index": 2, + "recipient_key_index": 3, + "account_reference": 195936478, + "created_at": 1700000000400, + "core_height_created_at": 2000000 + } + } +} diff --git a/packages/rs-platform-cxx/test_data/drive_query_vectors.json b/packages/rs-platform-cxx/test_data/drive_query_vectors.json new file mode 100644 index 00000000000..ac2b81815b8 --- /dev/null +++ b/packages/rs-platform-cxx/test_data/drive_query_vectors.json @@ -0,0 +1,480 @@ +{ + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "protocol_version": 12, + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "queries": [ + { + "name": "identity_balance", + "query": { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "grovedb_proof_hex": "0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "60" + ], + "key": "7777777777777777777777777777777777777777777777777777777777777777", + "element": "03fd00000002540be40000" + } + ], + "expected": { + "balance": 5000000000 + } + }, + { + "name": "identity_revision", + "query": { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "grovedb_proof_hex": "01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee012077777777777777777777777777777777777777777777777777777777777777770054017b204423f6e41b597890ac921d8f72453128cad7e268750f6fc87d648bcc72aa02cbeddd5e01bcb90c5002f6a710bc7957af406d2a6a9df600dbfa36951abf3534100301c0000b00080000000000000003001100", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "20", + "7777777777777777777777777777777777777777777777777777777777777777" + ], + "key": "c0", + "element": "0008000000000000000300" + } + ], + "expected": { + "revision": 3 + } + }, + { + "name": "identity_nonce", + "query": { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "grovedb_proof_hex": "01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee01207777777777777777777777777777777777777777777777777777777777777777007601b44b3a90d97444a14074c2bf69ff971d88fd27e3ba2da8b11cf3910f087dedf6030140000b00080000000000000007001002cbeddd5e01bcb90c5002f6a710bc7957af406d2a6a9df600dbfa36951abf3534100185d286a688ac17d96cdb2880a8f7174e151dfb90742370effeeaae391ee359df1100", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "20", + "7777777777777777777777777777777777777777777777777777777777777777" + ], + "key": "40", + "element": "0008000000000000000700" + } + ], + "expected": { + "nonce": 7 + } + }, + { + "name": "identity_contract_nonce", + "query": { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "contract_id": "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155" + }, + "grovedb_proof_hex": "01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee0120777777777777777777777777777777777777777777777777777777777777777700af0401200024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155008b708962e5e42245c8220f3a06dbcc9ba3a7e655df4fed3dba6e8cff9426f43d024e6a60642818a48d5f2e7c3647a08b69f48d05941c95aaa24536b810b3c49b681002cbeddd5e01bcb90c5002f6a710bc7957af406d2a6a9df600dbfa36951abf3534100185d286a688ac17d96cdb2880a8f7174e151dfb90742370effeeaae391ee359df1101012000490420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500050201010000411ce8bb44ce6a4f4169880f3111dbad167ccd51ab08199b006247e83b1fd1510120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c531550010030100000b0008000000000000000b0000", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "20", + "7777777777777777777777777777777777777777777777777777777777777777", + "20", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155" + ], + "key": "00", + "element": "0008000000000000000b00" + } + ], + "expected": { + "nonce": 11 + } + }, + { + "name": "identity_keys", + "query": { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "grovedb_proof_hex": "01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee01207777777777777777777777777777777777777777777777777777777777777777006e017b204423f6e41b597890ac921d8f72453128cad7e268750f6fc87d648bcc72aa04018000050201010100d8e4a96433656c05d1cfef00c047f123db4140e64fd3e509993825149a64a5d0100185d286a688ac17d96cdb2880a8f7174e151dfb90742370effeeaae391ee359df1101018000a1030100002d002a0000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa0000030101002d002a000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27000010030102003600330002010300000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687b001100", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "20", + "7777777777777777777777777777777777777777777777777777777777777777", + "80" + ], + "key": "00", + "element": "002a0000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa0000" + }, + { + "path": [ + "20", + "7777777777777777777777777777777777777777777777777777777777777777", + "80" + ], + "key": "01", + "element": "002a000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f270000" + }, + { + "path": [ + "20", + "7777777777777777777777777777777777777777777777777777777777777777", + "80" + ], + "key": "02", + "element": "00330002010300000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687b00" + } + ], + "expected": { + "serialized_keys": [ + "0000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa00", + "000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2700", + "0002010300000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687b" + ] + } + }, + { + "name": "identity_by_public_key_hash_present", + "query": { + "public_key_hash": "abababababababababababababababababababab" + }, + "grovedb_proof_hex": "0100810401180018020114abababababababababababababababababababab002b071a135a82f6729fb656781458f3bd429c7f938b2d2f7822dd9f8680b3e65c02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d11010118003b0314abababababababababababababababababababab0023002077777777777777777777777777777777777777777777777777777777777777770000", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "18" + ], + "key": "abababababababababababababababababababab", + "element": "0020777777777777777777777777777777777777777777777777777777777777777700" + } + ], + "expected": { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + } + }, + { + "name": "identity_by_public_key_hash_absent", + "query": { + "public_key_hash": "0101010101010101010101010101010101010101" + }, + "grovedb_proof_hex": "0100810401180018020114abababababababababababababababababababab002b071a135a82f6729fb656781458f3bd429c7f938b2d2f7822dd9f8680b3e65c02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101011800360514abababababababababababababababababababab6fe7c7f40ee008e6d6b5428b0bb240037607f9cd0ce8aa02ff921891148c977d00", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [], + "expected": { + "identity_id": null + } + }, + { + "name": "dpns_exact", + "grovedb_proof_hex": "0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b013af9b5c483d29ba0cb144b3c8d0b00acb6894c2ba54526ee525ecba262f25bbe0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000502010101005dfffca61718377c8e28755c8c4c41cd3281e43700cc17ef2fb2ce858f8d9e08100120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f040101000a020106646f6d61696e00aef97d6723e7dfdd1a37d4051a65d1851a7183ee391f51b154add1042479806001010100480406646f6d61696e001e02011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500c0db47fafbb7b914aca5112d697ebb013ac3f9de326d8135d68029a191e89cb20106646f6d61696e008a01bb6f7c477bd80200888c4db39eee8829d38f1ab953cc25c13f82c05fc5b25f32041a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500080201046461736800aeb4ddee9352a25186598ca471577e45325bd9cf1b02c80bb31d03ab58ba336810014fd3d5c7b045c62f2ac81c9f51f067a4450901d41999b0f82a3f15b103df4a6a11011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d65003b040464617368001302010f6e6f726d616c697a65644c6162656c000127fd05259b354ce1f58158158364d18a5a6c9f07227e9170a8ebda0e5e7fa3010464617368003c040f6e6f726d616c697a65644c6162656c0009020105616c696365001e7beb5cde43e295777634dc595f1536f62e2885343dca251fd9c3551285b8f6010f6e6f726d616c697a65644c6162656c002e0405616c69636500050201010000ecb69a79bd697e495ef181171cb50b0443bdc72b78e3c0446e75ae9f68ffeb2c0105616c696365003c0601000017001470726f7665642d64706e732d646f63756d656e7400a711904e44b1b6aec95a008065047732ea534162d3e3e0934617a6faa218739500", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "40", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "01", + "646f6d61696e", + "6e6f726d616c697a6564506172656e74446f6d61696e4e616d65", + "64617368", + "6e6f726d616c697a65644c6162656c", + "616c696365" + ], + "key": "00", + "element": "001470726f7665642d64706e732d646f63756d656e7400" + } + ], + "expected": { + "documents": [ + "70726f7665642d64706e732d646f63756d656e74" + ] + } + }, + { + "name": "dpns_prefix", + "grovedb_proof_hex": "0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b013af9b5c483d29ba0cb144b3c8d0b00acb6894c2ba54526ee525ecba262f25bbe0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000502010101005dfffca61718377c8e28755c8c4c41cd3281e43700cc17ef2fb2ce858f8d9e08100120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f040101000a020106646f6d61696e00aef97d6723e7dfdd1a37d4051a65d1851a7183ee391f51b154add1042479806001010100480406646f6d61696e001e02011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500c0db47fafbb7b914aca5112d697ebb013ac3f9de326d8135d68029a191e89cb20106646f6d61696e008a01bb6f7c477bd80200888c4db39eee8829d38f1ab953cc25c13f82c05fc5b25f32041a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500080201046461736800aeb4ddee9352a25186598ca471577e45325bd9cf1b02c80bb31d03ab58ba336810014fd3d5c7b045c62f2ac81c9f51f067a4450901d41999b0f82a3f15b103df4a6a11011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d65003b040464617368001302010f6e6f726d616c697a65644c6162656c000127fd05259b354ce1f58158158364d18a5a6c9f07227e9170a8ebda0e5e7fa3010464617368003c040f6e6f726d616c697a65644c6162656c0009020105616c696365001e7beb5cde43e295777634dc595f1536f62e2885343dca251fd9c3551285b8f6010f6e6f726d616c697a65644c6162656c002e0405616c69636500050201010000ecb69a79bd697e495ef181171cb50b0443bdc72b78e3c0446e75ae9f68ffeb2c0105616c696365003c0601000017001470726f7665642d64706e732d646f63756d656e7400a711904e44b1b6aec95a008065047732ea534162d3e3e0934617a6faa218739500", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "40", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "01", + "646f6d61696e", + "6e6f726d616c697a6564506172656e74446f6d61696e4e616d65", + "64617368", + "6e6f726d616c697a65644c6162656c", + "616c696365" + ], + "key": "00", + "element": "001470726f7665642d64706e732d646f63756d656e7400" + } + ], + "expected": { + "documents": [ + "70726f7665642d64706e732d646f63756d656e74" + ] + } + }, + { + "name": "dpns_by_identity", + "grovedb_proof_hex": "0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b013af9b5c483d29ba0cb144b3c8d0b00acb6894c2ba54526ee525ecba262f25bbe0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000502010101005dfffca61718377c8e28755c8c4c41cd3281e43700cc17ef2fb2ce858f8d9e08100120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f040101000a020106646f6d61696e00aef97d6723e7dfdd1a37d4051a65d1851a7183ee391f51b154add1042479806001010100480406646f6d61696e001e02011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500c0db47fafbb7b914aca5112d697ebb013ac3f9de326d8135d68029a191e89cb20106646f6d61696e009c01bb6f7c477bd80200888c4db39eee8829d38f1ab953cc25c13f82c05fc5b25f3202cec3da9112e0ef5667e59d93b88d238a89154e9e625b544c419a6455824d2a211004107265636f7264732e6964656e746974790024020120777777777777777777777777777777777777777777777777777777777777777700e1d05e567f8afe8c95122a35a3b9553f7120cd2407708db7af08ab1d90d6c51a1101107265636f7264732e6964656e74697479004904207777777777777777777777777777777777777777777777777777777777777777000502010100005379b2896c05a906d178f3dab0fe424000ef9e12147808701d6e30eef06425690120777777777777777777777777777777777777777777777777777777777777777700490401000024020120d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d100385ec810bbed70efa08b3e40b294d51af0bbe164a5266a5b54e18b8cb9e78566010100005b0620d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d10017001470726f7665642d64706e732d646f63756d656e7400a711904e44b1b6aec95a008065047732ea534162d3e3e0934617a6faa218739500", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "40", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "01", + "646f6d61696e", + "7265636f7264732e6964656e74697479", + "7777777777777777777777777777777777777777777777777777777777777777", + "00" + ], + "key": "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", + "element": "001470726f7665642d64706e732d646f63756d656e7400" + } + ], + "expected": { + "documents": [ + "70726f7665642d64706e732d646f63756d656e74" + ] + } + }, + { + "name": "dashpay_profile", + "grovedb_proof_hex": "0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b0420a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000502010101005b6467dac53627bd556c06b7de5e97bd2d50688788cc29c251144d866715c4bf02b8d9d2e8f03bb4b317f5fc24e2741a89ea9f5bb6cb43a44ebbf99bf056fc6105100120a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0030040101000b02010770726f66696c650084e1535a92501db14221531aacd7009467348130c1807153e9b3af041b75f61401010100520181acb67debb4c2c3aeaf6481bba22c23e59118b587c1adffcbe3c42e4754a6d5040770726f66696c650005020101000064064ce41f142aa0218b83251b9511d2d4affbb8012187cb1348f2062e61008710010770726f66696c65007202b077d63006fc15cfb2db05ddbee6d6dd7cf56291575215cee5216f7f1552a1250408246f776e657249640024020120777777777777777777777777777777777777777777777777777777777777777700282c1c78c773ac1bbcd84b55ad6bc8aa39653642a01744998b92045ce37949c7110108246f776e657249640049042077777777777777777777777777777777777777777777777777777777777777770005020101000071ea7f7cb98ab3eb3eae2895ce3a94c2714e806bb98c0e8c1177dde712022e8a01207777777777777777777777777777777777777777777777777777777777777777003f060100001a001770726f7665642d70726f66696c652d646f63756d656e7400da1581c51512f64b6e931eff13bd4fb05003f6befbbbc3b2962d208a582958df00", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "40", + "a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc", + "01", + "70726f66696c65", + "246f776e65724964", + "7777777777777777777777777777777777777777777777777777777777777777" + ], + "key": "00", + "element": "001770726f7665642d70726f66696c652d646f63756d656e7400" + } + ], + "expected": { + "documents": [ + "70726f7665642d70726f66696c652d646f63756d656e74" + ] + } + }, + { + "name": "dashpay_contacts_incoming", + "grovedb_proof_hex": "0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b0420a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000502010101005b6467dac53627bd556c06b7de5e97bd2d50688788cc29c251144d866715c4bf02b8d9d2e8f03bb4b317f5fc24e2741a89ea9f5bb6cb43a44ebbf99bf056fc6105100120a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0030040101000b02010770726f66696c650084e1535a92501db14221531aacd7009467348130c1807153e9b3af041b75f6140101010060040e636f6e7461637452657175657374000c020108246f776e6572496400c59531534d16dc4ec238c5c8a30fb6414b28cdbb977c82c77bfacf55ab8d3dcd028434a8d562d84df39d1971af05bd8fdd748581b35202e723eda1d4f737d7e93710010e636f6e74616374526571756573740094014b4c08659a34e5bccbbc817edffcec379dc3300ad974a50d539307d25bd505c202241ddcbc852479eec397f2fd32bcaf69ebadd79675ddbf92504d7e42436c1774100408746f5573657249640024020120777777777777777777777777777777777777777777777777777777777777777700902c3fed2d96edf795511e59795d4fa6fd6d36220a32a9aa13450e9562c5018c110108746f557365724964005204207777777777777777777777777777777777777777777777777777777777777777000e02010a2463726561746564417400c75709cd8917bb5116f06f15c1b88c22917b42e472c4f4ea2fecfe993291976b01207777777777777777777777777777777777777777777777777777777777777777003a040a24637265617465644174000c0201080000018bcfe56800005d8532c133405f01f9a65173c587d6a363a865b0c1e6fdc6a6b8b52d13f68575010a24637265617465644174003104080000018bcfe56800000502010100000dedbe3b16265a48243462cef6c2fbcdfcad43aed206f5310dbc84ad1a39d98901080000018bcfe5680000490401000024020120d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d300a2ab6c494b389894e8a4efe5a4e0673566880fd6a1d376962621108ced3d6120010100005e0620d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3001a001770726f7665642d636f6e746163742d646f63756d656e740007547e597229008c8eed5a86719458be289d9d088f8943866923c9c3cd230da400", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "40", + "a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc", + "01", + "636f6e7461637452657175657374", + "746f557365724964", + "7777777777777777777777777777777777777777777777777777777777777777", + "24637265617465644174", + "0000018bcfe56800", + "00" + ], + "key": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", + "element": "001770726f7665642d636f6e746163742d646f63756d656e7400" + } + ], + "expected": { + "documents": [ + "70726f7665642d636f6e746163742d646f63756d656e74" + ] + } + }, + { + "name": "dashpay_contacts_outgoing", + "grovedb_proof_hex": "0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b0420a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000502010101005b6467dac53627bd556c06b7de5e97bd2d50688788cc29c251144d866715c4bf02b8d9d2e8f03bb4b317f5fc24e2741a89ea9f5bb6cb43a44ebbf99bf056fc6105100120a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0030040101000b02010770726f66696c650084e1535a92501db14221531aacd7009467348130c1807153e9b3af041b75f6140101010060040e636f6e7461637452657175657374000c020108246f776e6572496400c59531534d16dc4ec238c5c8a30fb6414b28cdbb977c82c77bfacf55ab8d3dcd028434a8d562d84df39d1971af05bd8fdd748581b35202e723eda1d4f737d7e93710010e636f6e74616374526571756573740094014b4c08659a34e5bccbbc817edffcec379dc3300ad974a50d539307d25bd505c20408246f776e657249640024020120777777777777777777777777777777777777777777777777777777777777777700902c3fed2d96edf795511e59795d4fa6fd6d36220a32a9aa13450e9562c5018c1001dbbb47b9d5ef6862bed1e607946e8aea76b9fd7d77e408cb0d46ab6787d7bdb5110108246f776e65724964005204207777777777777777777777777777777777777777777777777777777777777777000e02010a2463726561746564417400c75709cd8917bb5116f06f15c1b88c22917b42e472c4f4ea2fecfe993291976b01207777777777777777777777777777777777777777777777777777777777777777003a040a24637265617465644174000c0201080000018bcfe56800005d8532c133405f01f9a65173c587d6a363a865b0c1e6fdc6a6b8b52d13f68575010a24637265617465644174003104080000018bcfe56800000502010100000dedbe3b16265a48243462cef6c2fbcdfcad43aed206f5310dbc84ad1a39d98901080000018bcfe5680000490401000024020120d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d300a2ab6c494b389894e8a4efe5a4e0673566880fd6a1d376962621108ced3d6120010100005e0620d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3001a001770726f7665642d636f6e746163742d646f63756d656e740007547e597229008c8eed5a86719458be289d9d088f8943866923c9c3cd230da400", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "40", + "a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc", + "01", + "636f6e7461637452657175657374", + "246f776e65724964", + "7777777777777777777777777777777777777777777777777777777777777777", + "24637265617465644174", + "0000018bcfe56800", + "00" + ], + "key": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", + "element": "001770726f7665642d636f6e746163742d646f63756d656e7400" + } + ], + "expected": { + "documents": [ + "70726f7665642d636f6e746163742d646f63756d656e74" + ] + } + }, + { + "name": "contested_vote_state_active", + "query": { + "normalized_label": "alice", + "count": 100 + }, + "grovedb_proof_hex": "0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee1911010464617368006f0405616c6963650024020120777777777777777777777777777777777777777777777777777777777777777700ba2fead8aaa37d1be007db47f2e9779b773387336b1fcb738551a2a8ca295ef201a5ddbf84df65edf5876f0141adfa01d1988f011c18020496c3e93599f57dd5d5110105616c69636500fb016703200000000000000000000000000000000000000000000000000000000000000000001b0018000003fd0000018bcfe56800fc0001e078fc001e84760400000420000000000000000000000000000000000000000000000000000000000000000100050201010100ac8e80c26488285c9ef7ce4167825d9e9bef26e737bb7f7f1340d69af3a8eb5910042000000000000000000000000000000000000000000000000000000000000000020005020101010012ec6171d927bc5d9f132f407acd3ab0e75049f021389f6430dd8d4ba87ce26d1104207777777777777777777777777777777777777777777777777777777777777777000502010100008bbee91750be1729b067ae752c218d46345cee65af3996e02cc4aa079d9b26fd10042088888888888888888888888888888888888888888888888888888888888888880005020101000092fab297d83548fabdbefbad2d5e3c26a4005332274f31ff9cd51c9436d401971104200000000000000000000000000000000000000000000000000000000000000001006b1c01010025040120c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c10400c1d2a7d2d0355e6a6e662719e9e8475444c47022e4f1eddddfb5b45a8686acea006a29c24ef5eb604e398952790112a98973d5485490ab94ec88844dd50ac7525100200000000000000000000000000000000000000000000000000000000000000002006b1c01010025040120c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2060035878fa5d0c12a20364a12f71fc2124b8ddc22d9d81368bd79b20400da6eee8800efe92f879d4c44f631bb52241d7606b85bbbab020cb55fd4395a887bd97101db00207777777777777777777777777777777777777777777777777777777777777777008d025ef5cd4abe01c5cc27c851dca48ca596628dfc45b454ee847bcfc824f17312b61c01010025040120a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a10a00f5b15a04f171a7a3fe3cf9dc367b0bfe597fea84cd7239384715570ce4551fe800a706a5fc391f817996f9e274f0b37ff0b8a61b579bf079a8da92846c9252d5511100208888888888888888888888888888888888888888888888888888888888888888008d02e6ee602462484dae8b0c314ddc157efc77b293ea2350cb615962612be3d768bf1c01010025040120b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b104002bb7188bd669db6e98f44ee502c9742d85da29c4e7824b4fbd27f9587ad2758a009b9c598e017cb597ea60903480c868289dd951ce282f67f97459c79366a9d0d31100", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "70", + "63", + "70", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "646f6d61696e", + "01", + "64617368", + "616c696365" + ], + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "element": "0018000003fd0000018bcfe56800fc0001e078fc001e8476040000" + }, + { + "path": [ + "70", + "63", + "70", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "646f6d61696e", + "01", + "64617368", + "616c696365", + "0000000000000000000000000000000000000000000000000000000000000001" + ], + "key": "01", + "element": "040120c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c10400" + }, + { + "path": [ + "70", + "63", + "70", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "646f6d61696e", + "01", + "64617368", + "616c696365", + "0000000000000000000000000000000000000000000000000000000000000002" + ], + "key": "01", + "element": "040120c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c20600" + }, + { + "path": [ + "70", + "63", + "70", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "646f6d61696e", + "01", + "64617368", + "616c696365", + "7777777777777777777777777777777777777777777777777777777777777777" + ], + "key": "01", + "element": "040120a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a10a00" + }, + { + "path": [ + "70", + "63", + "70", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "646f6d61696e", + "01", + "64617368", + "616c696365", + "8888888888888888888888888888888888888888888888888888888888888888" + ], + "key": "01", + "element": "040120b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b10400" + } + ], + "expected": { + "contenders": [ + { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "votes": 5 + }, + { + "identity_id": "8888888888888888888888888888888888888888888888888888888888888888", + "votes": 2 + } + ], + "abstain_votes": 2, + "lock_votes": 3, + "finished": false + } + }, + { + "name": "contested_vote_state_finished", + "query": { + "normalized_label": "bob", + "count": 100 + }, + "grovedb_proof_hex": "0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee1911010464617368006d0246f1e3eb733288ba8ee508b3b972872149b7ee87c6b0c05bbd4bb5982d93e7140403626f620024020120000000000000000000000000000000000000000000000000000000000000000000818e82b602d0fff0c045f136090f3816039b90af1ac6f33f3ec84deba88908d6110103626f6200fb018403200000000000000000000000000000000000000000000000000000000000000000016000fb015b00010400777777777777777777777777777777777777777777777777777777777777777702a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a104a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a20100888888888888888888888888888888888888888888888888888888888888888801b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1020101c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1020201c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c201fd0000018bcfe56800fc0001e078fc001e847604fd0000018bcfed0920fc0001e208fc001e847f04017777777777777777777777777777777777777777777777777777777777777777017777777777777777777777777777777777777777777777777777777777777777000000", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [ + { + "path": [ + "70", + "63", + "70", + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "646f6d61696e", + "01", + "64617368", + "626f62" + ], + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "element": "00fb015b00010400777777777777777777777777777777777777777777777777777777777777777702a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a104a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a20100888888888888888888888888888888888888888888888888888888888888888801b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1020101c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1020201c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c201fd0000018bcfe56800fc0001e078fc001e847604fd0000018bcfed0920fc0001e208fc001e847f040177777777777777777777777777777777777777777777777777777777777777770177777777777777777777777777777777777777777777777777777777777777770000" + } + ], + "stored_info_hex": "00010400777777777777777777777777777777777777777777777777777777777777777702a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a104a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a20100888888888888888888888888888888888888888888888888888888888888888801b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1020101c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1020201c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c201fd0000018bcfe56800fc0001e078fc001e847604fd0000018bcfed0920fc0001e208fc001e847f0401777777777777777777777777777777777777777777777777777777777777777701777777777777777777777777777777777777777777777777777777777777777700", + "expected": { + "contenders": [ + { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "votes": 5 + }, + { + "identity_id": "8888888888888888888888888888888888888888888888888888888888888888", + "votes": 2 + } + ], + "abstain_votes": 2, + "lock_votes": 1, + "finished": true, + "winner_identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + } + }, + { + "name": "contested_vote_state_absent", + "query": { + "normalized_label": "carol", + "count": 100 + }, + "grovedb_proof_hex": "0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee191101046461736800470246f1e3eb733288ba8ee508b3b972872149b7ee87c6b0c05bbd4bb5982d93e7140503626f62818e82b602d0fff0c045f136090f3816039b90af1ac6f33f3ec84deba88908d61100", + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "results": [], + "expected": { + "contest": null + } + } + ] +} diff --git a/packages/rs-platform-cxx/test_data/quorum_sig_vectors.json b/packages/rs-platform-cxx/test_data/quorum_sig_vectors.json new file mode 100644 index 00000000000..52ea2ca59f6 --- /dev/null +++ b/packages/rs-platform-cxx/test_data/quorum_sig_vectors.json @@ -0,0 +1,125 @@ +{ + "tenderdash_abci_tag": "v1.5.1", + "tenderdash_proto_tag": "v1.5.3", + "bls_scheme": "basic", + "vectors": [ + { + "name": "valid", + "app_hash": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "envelope": { + "grovedb_proof_hex": "0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000", + "quorum_type": 106, + "quorum_hash": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366", + "round": 0, + "signature": "a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319" + }, + "quorum_public_key": "b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15", + "block_context": { + "height": 123456, + "round": 0, + "core_chain_locked_height": 2000000, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "intermediates": { + "request_id": "e013e333e6ce445bc0979595c72f4e8e4da913804961d265c3aee4a33c33642b", + "state_id_bytes": "42090c000000000000001140e20100000000001a20dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab722580841e00290068e5cf8b010000", + "state_id_hash": "6ae0fe80f2f5e586528f2df34e13e14433fd5032b85ed2ad407142c30476d66e", + "canonical_vote_bytes": "0200000040e20100000000000000000000000000090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d6063666ae0fe80f2f5e586528f2df34e13e14433fd5032b85ed2ad407142c30476d66e646173682d746573746e65742d3531", + "vote_hash": "a8ecf92e5990a7114be6090453907309ed6830aae1a940e8af93c25193c010ea", + "sign_digest": "304253ac4ac97e088ca8648ceb2d897e5867d24ced6cc25de51f102f27b3523f" + }, + "expected_valid": true + }, + { + "name": "tampered_signature", + "app_hash": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "envelope": { + "grovedb_proof_hex": "0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000", + "quorum_type": 106, + "quorum_hash": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366", + "round": 0, + "signature": "a4fca3d223938deeb9da83d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319" + }, + "quorum_public_key": "b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15", + "block_context": { + "height": 123456, + "round": 0, + "core_chain_locked_height": 2000000, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "expected_valid": false + }, + { + "name": "wrong_quorum_key", + "app_hash": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "envelope": { + "grovedb_proof_hex": "0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000", + "quorum_type": 106, + "quorum_hash": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366", + "round": 0, + "signature": "a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319" + }, + "quorum_public_key": "99d85ad48c7ca9ffbe49f170e444a77ca98035a446c0fe9919180860757ef0d73e8a04abb18f5b67558baed45e748100", + "block_context": { + "height": 123456, + "round": 0, + "core_chain_locked_height": 2000000, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "expected_valid": false + }, + { + "name": "wrong_app_hash", + "app_hash": "25d905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "envelope": { + "grovedb_proof_hex": "0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000", + "quorum_type": 106, + "quorum_hash": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366", + "round": 0, + "signature": "a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319" + }, + "quorum_public_key": "b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15", + "block_context": { + "height": 123456, + "round": 0, + "core_chain_locked_height": 2000000, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "expected_valid": false + }, + { + "name": "wrong_block_id_hash", + "app_hash": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72", + "envelope": { + "grovedb_proof_hex": "0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000", + "quorum_type": 106, + "quorum_hash": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash": "090c0f1215171b1e2124272a2d303336393c3f4245484b4e5154575a5d606366", + "round": 0, + "signature": "a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319" + }, + "quorum_public_key": "b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15", + "block_context": { + "height": 123456, + "round": 0, + "core_chain_locked_height": 2000000, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "expected_valid": false + } + ] +} diff --git a/packages/rs-platform-cxx/tests/cxx_smoke.cc b/packages/rs-platform-cxx/tests/cxx_smoke.cc new file mode 100644 index 00000000000..4c3b31b146d --- /dev/null +++ b/packages/rs-platform-cxx/tests/cxx_smoke.cc @@ -0,0 +1,13 @@ +#include + +#include + +int main() +{ + try { + platform_ffi::set_context("test", std::uint32_t{0}); + } catch (const rust::Error&) { + return 1; + } + return 0; +} diff --git a/packages/rs-platform-cxx/tests/decoders.rs b/packages/rs-platform-cxx/tests/decoders.rs new file mode 100644 index 00000000000..5061901513a --- /dev/null +++ b/packages/rs-platform-cxx/tests/decoders.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Decoder tests against the rs-dpp-generated vectors in +//! `test_data/dpp_identity_vectors.json` (identities) and the +//! stored-document fixtures in dpp_st_vectors.json (DPNS domain, DashPay +//! profile and contactRequest documents as Drive stores/returns them). + +use dash_platform_cxx::decode; +use serde_json::Value; + +fn hexv(hex: &str) -> Vec { + hex::decode(hex).expect("bad hex in test vector") +} + +fn identity_vectors() -> Value { + serde_json::from_str(include_str!("../test_data/dpp_identity_vectors.json")) + .expect("parse dpp_identity_vectors.json") +} + +fn st_vectors() -> Value { + serde_json::from_str(include_str!("../test_data/dpp_st_vectors.json")) + .expect("parse dpp_st_vectors.json") +} + +#[test] +fn decode_identity_vector() { + let doc = identity_vectors(); + let vector = &doc["identity"]; + let identity = decode::decode_identity(&hexv(vector["serialized_hex"].as_str().unwrap())) + .expect("decode identity"); + + assert_eq!(hex::encode(identity.id), vector["id"].as_str().unwrap()); + assert_eq!(Some(identity.balance), vector["balance"].as_u64()); + assert_eq!(Some(identity.revision), vector["revision"].as_u64()); + + let expected_keys = vector["public_keys"].as_array().unwrap(); + assert_eq!(identity.keys.len(), expected_keys.len()); + for (key, expected) in identity.keys.iter().zip(expected_keys) { + assert_eq!(Some(u64::from(key.id)), expected["id"].as_u64()); + assert_eq!(Some(u64::from(key.purpose)), expected["purpose"].as_u64()); + assert_eq!( + Some(u64::from(key.security_level)), + expected["security_level"].as_u64() + ); + assert_eq!(Some(u64::from(key.key_type)), expected["key_type"].as_u64()); + assert_eq!(Some(key.read_only), expected["read_only"].as_bool()); + assert_eq!(hex::encode(&key.data), expected["data"].as_str().unwrap()); + assert_eq!(key.disabled_at, expected["disabled_at"].as_u64()); + } +} + +#[test] +fn decode_identity_rejects_garbage() { + assert!(decode::decode_identity(&[0xff; 16]).is_err()); +} + +#[test] +fn decode_stored_dpns_domain() { + let doc = st_vectors(); + let vector = &doc["stored_documents"]["domain"]; + let name = decode::decode_dpns_domain(&hexv(vector["serialized_hex"].as_str().unwrap())) + .expect("decode DPNS domain"); + assert_eq!( + hex::encode(name.document_id), + vector["id_hex"].as_str().unwrap() + ); + assert_eq!( + hex::encode(name.owner_id), + vector["owner_id_hex"].as_str().unwrap() + ); + assert_eq!( + hex::encode(name.identity), + vector["identity_id_hex"].as_str().unwrap() + ); + assert_eq!(name.label, vector["label"].as_str().unwrap()); + assert_eq!( + name.normalized_label, + vector["normalized_label"].as_str().unwrap() + ); + assert_eq!(name.parent_domain, "dash"); +} + +#[test] +fn decode_stored_dashpay_profile() { + let doc = st_vectors(); + let vector = &doc["stored_documents"]["profile"]; + let profile = decode::decode_dashpay_profile(&hexv(vector["serialized_hex"].as_str().unwrap())) + .expect("decode DashPay profile"); + assert_eq!( + hex::encode(profile.document_id), + vector["id_hex"].as_str().unwrap() + ); + assert_eq!( + hex::encode(profile.owner_id), + vector["owner_id_hex"].as_str().unwrap() + ); + assert_eq!(Some(profile.revision), vector["revision"].as_u64()); + assert_eq!( + profile.display_name, + vector["display_name"].as_str().unwrap() + ); + assert_eq!( + profile.public_message, + vector["public_message"].as_str().unwrap() + ); + assert_eq!(profile.avatar_url, vector["avatar_url"].as_str().unwrap()); + assert_eq!(Some(profile.created_at), vector["created_at"].as_u64()); + assert_eq!(Some(profile.updated_at), vector["updated_at"].as_u64()); + assert_eq!(profile.avatar_hash, vec![0x88; 32]); + assert_eq!(profile.avatar_fingerprint, vec![0x99; 8]); +} + +#[test] +fn decode_stored_contact_request() { + let doc = st_vectors(); + let vector = &doc["stored_documents"]["contact"]; + let request = decode::decode_contact_request(&hexv(vector["serialized_hex"].as_str().unwrap())) + .expect("decode contact request"); + assert_eq!( + hex::encode(request.document_id), + vector["id_hex"].as_str().unwrap() + ); + assert_eq!( + hex::encode(request.owner_id), + vector["owner_id_hex"].as_str().unwrap() + ); + assert_eq!( + hex::encode(request.to_user_id), + vector["to_user_id_hex"].as_str().unwrap() + ); + assert_eq!( + Some(u64::from(request.sender_key_index)), + vector["sender_key_index"].as_u64() + ); + assert_eq!( + Some(u64::from(request.recipient_key_index)), + vector["recipient_key_index"].as_u64() + ); + assert_eq!( + Some(u64::from(request.account_reference)), + vector["account_reference"].as_u64() + ); + assert_eq!(Some(request.created_at), vector["created_at"].as_u64()); + assert_eq!( + Some(u64::from(request.core_height_created_at)), + vector["core_height_created_at"].as_u64() + ); + assert_eq!(request.encrypted_public_key, vec![0xab; 96]); + assert_eq!(request.encrypted_account_label, vec![0xcd; 48]); +} + +// Documents proven by the drive query vectors are placeholder items, not +// real documents; decoding them must fail cleanly rather than panic. +#[test] +fn decode_rejects_placeholder_documents() { + assert!(decode::decode_dpns_domain(b"proved-dpns-document").is_err()); + assert!(decode::decode_dashpay_profile(b"proved-profile-document").is_err()); + assert!(decode::decode_contact_request(b"proved-contact-document").is_err()); +} diff --git a/packages/rs-platform-cxx/tests/from_proof.rs b/packages/rs-platform-cxx/tests/from_proof.rs new file mode 100644 index 00000000000..23393cde64d --- /dev/null +++ b/packages/rs-platform-cxx/tests/from_proof.rs @@ -0,0 +1,690 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Acceptance tests for the FromProof-driven (request bytes, response bytes) +//! verification seam: synthesize the DAPI protobuf request/response pairs +//! the C++ transport exchanges — grovedb proofs and the quorum signature +//! come from the package fixture corpus — push the +//! fixture quorum key through the provider store, and verify end to end +//! (grovedb replay + Tenderdash BLS quorum signature). +//! +//! Every fixture proof commits to the same root hash and the fixture quorum +//! signed exactly that root, so all positive cases run the full pipeline. +//! The fixture grovedb state stores placeholder payloads at document +//! positions; document queries therefore pin a clean decode failure (the +//! upstream corpus pins the same), while the identity and contested-vote +//! families verify positively. + +use std::sync::Once; + +use dapi_grpc::platform::v0::{self as proto, Proof, ResponseMetadata}; +use prost::Message; +use serde::Deserialize; +use serde_json::Value; + +#[derive(Deserialize)] +struct DriveVectorFile { + identity_id: String, + queries: Vec, +} + +#[derive(Deserialize)] +struct QueryVector { + name: String, + #[serde(default)] + query: Value, + grovedb_proof_hex: String, + expected: Value, +} + +#[derive(Deserialize)] +struct QuorumVectorFile { + vectors: Vec, +} + +#[derive(Deserialize)] +struct QuorumVector { + name: String, + envelope: Envelope, + quorum_public_key: String, + block_context: BlockContext, +} + +#[derive(Deserialize)] +struct Envelope { + quorum_type: u32, + quorum_hash: String, + block_id_hash: String, + round: u32, + signature: String, +} + +#[derive(Deserialize)] +struct BlockContext { + height: u64, + core_chain_locked_height: u32, + time_ms: u64, + protocol_version: u32, + chain_id: String, +} + +fn hexv(hex: &str) -> Vec { + hex::decode(hex).expect("bad hex in test vector") +} + +fn drive_vectors() -> DriveVectorFile { + serde_json::from_str(include_str!("../test_data/drive_query_vectors.json")) + .expect("parse drive_query_vectors.json") +} + +fn quorum_valid() -> QuorumVector { + let file: QuorumVectorFile = + serde_json::from_str(include_str!("../test_data/quorum_sig_vectors.json")) + .expect("parse quorum_sig_vectors.json"); + file.vectors + .into_iter() + .find(|vector| vector.name == "valid") + .expect("valid quorum vector") +} + +fn query<'a>(file: &'a DriveVectorFile, name: &str) -> &'a QueryVector { + file.queries + .iter() + .find(|query| query.name == name) + .unwrap_or_else(|| panic!("query vector not found: {name}")) +} + +/// Installs the fixture context exactly once per test binary: the network +/// the fixture chain id belongs to and the fixture quorum key. Tests that +/// need a failing key lookup tamper the response's quorum hash instead of +/// mutating this shared store. +fn setup() { + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + dash_platform_cxx::provider::set_context("test", 0).expect("set_context"); + let quorum = quorum_valid(); + dash_platform_cxx::provider::update_quorum_keys( + quorum.envelope.quorum_type as u8, + vec![dash_platform_cxx::provider::QuorumKey { + quorum_hash: hexv(&quorum.envelope.quorum_hash) + .try_into() + .expect("32-byte quorum hash"), + public_key: hexv(&quorum.quorum_public_key) + .try_into() + .expect("48-byte quorum key"), + }], + ); + }); +} + +/// The proof envelope binding `grovedb_proof` to the fixture-signed block. +fn proof_msg(grovedb_proof: Vec) -> Proof { + let quorum = quorum_valid(); + Proof { + grovedb_proof, + quorum_hash: hexv(&quorum.envelope.quorum_hash), + signature: hexv(&quorum.envelope.signature), + round: quorum.envelope.round, + block_id_hash: hexv(&quorum.envelope.block_id_hash), + quorum_type: quorum.envelope.quorum_type, + } +} + +fn metadata() -> ResponseMetadata { + let ctx = quorum_valid().block_context; + ResponseMetadata { + height: ctx.height, + core_chain_locked_height: ctx.core_chain_locked_height, + epoch: 0, + time_ms: ctx.time_ms, + protocol_version: ctx.protocol_version, + chain_id: ctx.chain_id, + } +} + +fn check_meta(meta: &dash_platform_cxx::types::Meta) { + let ctx = quorum_valid().block_context; + assert_eq!(meta.height, ctx.height); + assert_eq!(meta.core_chain_locked_height, ctx.core_chain_locked_height); + assert_eq!(meta.time_ms, ctx.time_ms); + assert_eq!(meta.protocol_version, ctx.protocol_version); + assert_eq!(meta.chain_id, ctx.chain_id); +} + +// --- request/response synthesis used by transport adapters + +fn identity_nonce_request(identity_id: Vec) -> Vec { + proto::GetIdentityNonceRequest { + version: Some(proto::get_identity_nonce_request::Version::V0( + proto::get_identity_nonce_request::GetIdentityNonceRequestV0 { + identity_id, + prove: true, + }, + )), + } + .encode_to_vec() +} + +fn identity_nonce_response(proof: Proof) -> Vec { + proto::GetIdentityNonceResponse { + version: Some(proto::get_identity_nonce_response::Version::V0( + proto::get_identity_nonce_response::GetIdentityNonceResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_identity_nonce_response::get_identity_nonce_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +fn identity_contract_nonce_request(identity_id: Vec, contract_id: Vec) -> Vec { + proto::GetIdentityContractNonceRequest { + version: Some(proto::get_identity_contract_nonce_request::Version::V0( + proto::get_identity_contract_nonce_request::GetIdentityContractNonceRequestV0 { + identity_id, + contract_id, + prove: true, + }, + )), + } + .encode_to_vec() +} + +fn identity_contract_nonce_response(proof: Proof) -> Vec { + proto::GetIdentityContractNonceResponse { + version: Some(proto::get_identity_contract_nonce_response::Version::V0( + proto::get_identity_contract_nonce_response::GetIdentityContractNonceResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_identity_contract_nonce_response::get_identity_contract_nonce_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +fn identity_balance_request(identity_id: Vec) -> Vec { + proto::GetIdentityBalanceRequest { + version: Some(proto::get_identity_balance_request::Version::V0( + proto::get_identity_balance_request::GetIdentityBalanceRequestV0 { + id: identity_id, + prove: true, + }, + )), + } + .encode_to_vec() +} + +fn identity_balance_response(proof: Proof) -> Vec { + proto::GetIdentityBalanceResponse { + version: Some(proto::get_identity_balance_response::Version::V0( + proto::get_identity_balance_response::GetIdentityBalanceResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_identity_balance_response::get_identity_balance_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +fn identity_keys_request(identity_id: Vec) -> Vec { + proto::GetIdentityKeysRequest { + version: Some(proto::get_identity_keys_request::Version::V0( + proto::get_identity_keys_request::GetIdentityKeysRequestV0 { + identity_id, + request_type: Some(proto::KeyRequestType { + request: Some(proto::key_request_type::Request::AllKeys(proto::AllKeys {})), + }), + limit: None, + offset: None, + prove: true, + }, + )), + } + .encode_to_vec() +} + +fn identity_keys_response(proof: Proof) -> Vec { + proto::GetIdentityKeysResponse { + version: Some(proto::get_identity_keys_response::Version::V0( + proto::get_identity_keys_response::GetIdentityKeysResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_identity_keys_response::get_identity_keys_response_v0::Result::Proof( + proof, + ), + ), + }, + )), + } + .encode_to_vec() +} + +/// bincode (standard config) encoding of a platform Value::Text, as +/// drive-abci decodes contested index values (discriminant 18 + length + +/// utf8; labels are short so the length is a single byte). +fn bincode_text(text: &str) -> Vec { + let mut out = vec![18u8, u8::try_from(text.len()).expect("short label")]; + out.extend_from_slice(text.as_bytes()); + out +} + +fn contested_request(contract_id: Vec, normalized_label: &str, count: u32) -> Vec { + proto::GetContestedResourceVoteStateRequest { + version: Some(proto::get_contested_resource_vote_state_request::Version::V0( + proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 { + contract_id, + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: vec![bincode_text("dash"), bincode_text(normalized_label)], + result_type: + proto::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::ResultType::VoteTally + .into(), + allow_include_locked_and_abstaining_vote_tally: true, + start_at_identifier_info: None, + count: Some(count), + prove: true, + }, + )), + } + .encode_to_vec() +} + +fn contested_response(proof: Proof) -> Vec { + proto::GetContestedResourceVoteStateResponse { + version: Some(proto::get_contested_resource_vote_state_response::Version::V0( + proto::get_contested_resource_vote_state_response::GetContestedResourceVoteStateResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_contested_resource_vote_state_response::get_contested_resource_vote_state_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +/// CBOR where/order-by clauses exactly as the C++ transport encodes them +/// (transport/cbor.h): definite-length arrays of [field, operator, value] +/// triples. +fn dpns_exact_documents_request(contract_id: Vec, normalized_label: &str) -> Vec { + let clauses = vec![ + vec![ + ciborium::Value::Text("normalizedParentDomainName".to_string()), + ciborium::Value::Text("==".to_string()), + ciborium::Value::Text("dash".to_string()), + ], + vec![ + ciborium::Value::Text("normalizedLabel".to_string()), + ciborium::Value::Text("==".to_string()), + ciborium::Value::Text(normalized_label.to_string()), + ], + ]; + let cbor = ciborium::Value::Array( + clauses + .into_iter() + .map(ciborium::Value::Array) + .collect::>(), + ); + let mut where_bytes = Vec::new(); + ciborium::into_writer(&cbor, &mut where_bytes).expect("encode where clauses"); + proto::GetDocumentsRequest { + version: Some(proto::get_documents_request::Version::V0( + proto::get_documents_request::GetDocumentsRequestV0 { + data_contract_id: contract_id, + document_type: "domain".to_string(), + r#where: where_bytes, + order_by: Vec::new(), + limit: 1, + prove: true, + start: None, + }, + )), + } + .encode_to_vec() +} + +fn documents_response(proof: Proof) -> Vec { + proto::GetDocumentsResponse { + version: Some(proto::get_documents_response::Version::V0( + proto::get_documents_response::GetDocumentsResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_documents_response::get_documents_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +const DPNS_CONTRACT_ID_HEX: &str = + "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155"; + +// --- positive cases -------------------------------------------------------- + +#[test] +fn identity_nonce_verifies_end_to_end() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_nonce"); + let request = identity_nonce_request(hexv(&file.identity_id)); + let response = identity_nonce_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (nonce, meta) = dash_platform_cxx::verify::verify_get_identity_nonce(&request, &response) + .expect("identity nonce verifies"); + assert_eq!(nonce, q.expected["nonce"].as_u64()); + check_meta(&meta); +} + +#[test] +fn identity_contract_nonce_verifies_end_to_end() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_contract_nonce"); + let contract_id = hexv(q.query["contract_id"].as_str().expect("contract_id")); + let request = identity_contract_nonce_request(hexv(&file.identity_id), contract_id); + let response = identity_contract_nonce_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (nonce, meta) = + dash_platform_cxx::verify::verify_get_identity_contract_nonce(&request, &response) + .expect("identity contract nonce verifies"); + assert_eq!(nonce, q.expected["nonce"].as_u64()); + check_meta(&meta); +} + +#[test] +fn identity_balance_verifies_end_to_end() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_balance"); + let request = identity_balance_request(hexv(&file.identity_id)); + let response = identity_balance_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (balance, meta) = + dash_platform_cxx::verify::verify_get_identity_balance(&request, &response) + .expect("identity balance verifies"); + assert_eq!(balance, q.expected["balance"].as_u64()); + check_meta(&meta); +} + +#[test] +fn identity_keys_verify_end_to_end() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_keys"); + let request = identity_keys_request(hexv(&file.identity_id)); + let response = identity_keys_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (keys, meta) = dash_platform_cxx::verify::verify_get_identity_keys(&request, &response) + .expect("identity keys verify"); + let keys = keys.expect("identity proven present"); + assert_eq!( + keys.iter().map(|key| key.id).collect::>(), + vec![0, 1, 2] + ); + assert_eq!( + keys.len(), + q.expected["serialized_keys"] + .as_array() + .expect("keys") + .len() + ); + check_meta(&meta); +} + +#[test] +fn contested_vote_state_active_verifies_end_to_end() { + setup(); + let file = drive_vectors(); + let q = query(&file, "contested_vote_state_active"); + let label = q.query["normalized_label"].as_str().expect("label"); + let count = q.query["count"].as_u64().expect("count") as u32; + let request = contested_request(hexv(DPNS_CONTRACT_ID_HEX), label, count); + let response = contested_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (state, meta) = + dash_platform_cxx::verify::verify_get_contested_vote_state(&request, &response) + .expect("contested vote state verifies"); + check_meta(&meta); + assert!(state.contest_found); + assert!(!state.finished); + let expected_contenders = q.expected["contenders"].as_array().unwrap(); + assert_eq!(state.contenders.len(), expected_contenders.len()); + for ((identity, votes), expected) in state.contenders.iter().zip(expected_contenders) { + assert_eq!( + hex::encode(identity), + expected["identity_id"].as_str().unwrap() + ); + assert_eq!(votes.map(u64::from), expected["votes"].as_u64()); + } + assert_eq!( + state.abstain_votes.map(u64::from), + q.expected["abstain_votes"].as_u64() + ); + assert_eq!( + state.lock_votes.map(u64::from), + q.expected["lock_votes"].as_u64() + ); +} + +#[test] +fn contested_vote_state_finished_verifies_end_to_end() { + setup(); + let file = drive_vectors(); + let q = query(&file, "contested_vote_state_finished"); + let label = q.query["normalized_label"].as_str().expect("label"); + let count = q.query["count"].as_u64().expect("count") as u32; + let request = contested_request(hexv(DPNS_CONTRACT_ID_HEX), label, count); + let response = contested_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (state, _) = + dash_platform_cxx::verify::verify_get_contested_vote_state(&request, &response) + .expect("contested vote state verifies"); + assert!(state.contest_found); + assert!(state.finished); + assert!(!state.locked); + assert_eq!( + state.winner.map(hex::encode), + q.expected["winner_identity_id"].as_str().map(String::from) + ); + assert!(state.finished_at_time_ms > 0); +} + +#[test] +fn contested_vote_state_absent_is_proven() { + setup(); + let file = drive_vectors(); + let q = query(&file, "contested_vote_state_absent"); + let label = q.query["normalized_label"].as_str().expect("label"); + let count = q.query["count"].as_u64().expect("count") as u32; + let request = contested_request(hexv(DPNS_CONTRACT_ID_HEX), label, count); + let response = contested_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let (state, _) = + dash_platform_cxx::verify::verify_get_contested_vote_state(&request, &response) + .expect("contested vote state verifies"); + assert!(!state.contest_found); + assert!(state.contenders.is_empty()); +} + +// --- decode-failure pin ---------------------------------------------------- + +/// The fixture grovedb state stores placeholder payloads at document +/// positions: the grovedb + query-shape verification succeeds, and the +/// document decode must fail cleanly (an Err, never a panic). The upstream +/// rs-drive-proof-verifier corpus pins the same behavior. +#[test] +fn placeholder_documents_fail_decoding_cleanly() { + setup(); + let file = drive_vectors(); + let q = query(&file, "dpns_exact"); + let request = dpns_exact_documents_request(hexv(DPNS_CONTRACT_ID_HEX), "alice"); + let response = documents_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let err = dash_platform_cxx::verify::verify_get_documents(&request, &response) + .expect_err("placeholder documents must not decode"); + // The failure must come from the DPP decode, not from the grovedb + // replay: a query-shape drift would surface as a "grovedb:" proof error. + assert!( + !err.contains("grovedb"), + "unexpected proof-layer error: {err}" + ); +} + +// --- negative cases (quorum binding) --------------------------------------- + +#[test] +fn tampered_signature_is_rejected() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_nonce"); + let mut proof = proof_msg(hexv(&q.grovedb_proof_hex)); + proof.signature[10] ^= 0x01; + let request = identity_nonce_request(hexv(&file.identity_id)); + let response = identity_nonce_response(proof); + dash_platform_cxx::verify::verify_get_identity_nonce(&request, &response) + .expect_err("tampered signature must fail"); +} + +#[test] +fn unknown_quorum_is_rejected() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_nonce"); + let mut proof = proof_msg(hexv(&q.grovedb_proof_hex)); + proof.quorum_hash[0] ^= 0x01; + let request = identity_nonce_request(hexv(&file.identity_id)); + let response = identity_nonce_response(proof); + let err = dash_platform_cxx::verify::verify_get_identity_nonce(&request, &response) + .expect_err("unknown quorum must fail"); + assert!(err.contains("quorum"), "unexpected error: {err}"); +} + +#[test] +fn tampered_grovedb_proof_is_rejected() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_nonce"); + let mut grovedb_proof = hexv(&q.grovedb_proof_hex); + let mid = grovedb_proof.len() / 2; + grovedb_proof[mid] ^= 0x01; + let request = identity_nonce_request(hexv(&file.identity_id)); + let response = identity_nonce_response(proof_msg(grovedb_proof)); + dash_platform_cxx::verify::verify_get_identity_nonce(&request, &response) + .expect_err("tampered grovedb proof must fail"); +} + +/// The signed metadata is part of the quorum-signature preimage: a replayed +/// proof with altered height must fail even though the signature itself is +/// valid for the original block. +#[test] +fn tampered_metadata_height_is_rejected() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_nonce"); + let request = identity_nonce_request(hexv(&file.identity_id)); + let mut mtd = metadata(); + mtd.height += 1; + let response = proto::GetIdentityNonceResponse { + version: Some(proto::get_identity_nonce_response::Version::V0( + proto::get_identity_nonce_response::GetIdentityNonceResponseV0 { + metadata: Some(mtd), + result: Some( + proto::get_identity_nonce_response::get_identity_nonce_response_v0::Result::Proof( + proof_msg(hexv(&q.grovedb_proof_hex)), + ), + ), + }, + )), + } + .encode_to_vec(); + dash_platform_cxx::verify::verify_get_identity_nonce(&request, &response) + .expect_err("tampered signed height must fail"); +} + +fn identity_request(id: Vec) -> Vec { + proto::GetIdentityRequest { + version: Some(proto::get_identity_request::Version::V0( + proto::get_identity_request::GetIdentityRequestV0 { id, prove: true }, + )), + } + .encode_to_vec() +} + +fn identity_response(proof: Proof) -> Vec { + proto::GetIdentityResponse { + version: Some(proto::get_identity_response::Version::V0( + proto::get_identity_response::GetIdentityResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_identity_response::get_identity_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +fn identity_by_pubkey_hash_request(public_key_hash: Vec) -> Vec { + proto::GetIdentityByPublicKeyHashRequest { + version: Some(proto::get_identity_by_public_key_hash_request::Version::V0( + proto::get_identity_by_public_key_hash_request::GetIdentityByPublicKeyHashRequestV0 { + public_key_hash, + prove: true, + }, + )), + } + .encode_to_vec() +} + +fn identity_by_pubkey_hash_response(proof: Proof) -> Vec { + proto::GetIdentityByPublicKeyHashResponse { + version: Some(proto::get_identity_by_public_key_hash_response::Version::V0( + proto::get_identity_by_public_key_hash_response::GetIdentityByPublicKeyHashResponseV0 { + metadata: Some(metadata()), + result: Some( + proto::get_identity_by_public_key_hash_response::get_identity_by_public_key_hash_response_v0::Result::Proof(proof), + ), + }, + )), + } + .encode_to_vec() +} + +/// The fixture corpus has no full-identity proof: the pubkey-hash vectors +/// prove only the unique-hash -> identity-id mapping, while the verifier +/// requires the full identity subtree (upstream skips these vectors for the +/// same reason). Until a full-identity fixture is regenerated, the two +/// flagship identity paths are pinned negatively: an id-mapping-only proof +/// must be rejected cleanly, never verified and never a panic. +#[test] +fn identity_by_pubkey_hash_rejects_id_mapping_only_proof() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_by_public_key_hash_present"); + let request = + identity_by_pubkey_hash_request(hexv(q.query["public_key_hash"].as_str().unwrap())); + let response = identity_by_pubkey_hash_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let err = dash_platform_cxx::verify::verify_get_identity_by_pubkey_hash(&request, &response) + .expect_err("id-mapping-only proof must not satisfy full-identity verification"); + assert!( + err.contains("identity-by-public-key-hash proof verification failed"), + "unexpected error: {err}" + ); +} + +/// A structurally valid proof for a different query shape must fail cleanly +/// when presented as a full-identity proof. +#[test] +fn identity_rejects_wrong_shape_proof() { + setup(); + let file = drive_vectors(); + let q = query(&file, "identity_nonce"); + let request = identity_request(hexv(&file.identity_id)); + let response = identity_response(proof_msg(hexv(&q.grovedb_proof_hex))); + let err = dash_platform_cxx::verify::verify_get_identity(&request, &response) + .expect_err("nonce-shaped proof must not satisfy full-identity verification"); + assert!( + err.contains("identity proof verification failed"), + "unexpected error: {err}" + ); +} diff --git a/packages/rs-platform-cxx/tests/signing.rs b/packages/rs-platform-cxx/tests/signing.rs new file mode 100644 index 00000000000..4a6043b1dcb --- /dev/null +++ b/packages/rs-platform-cxx/tests/signing.rs @@ -0,0 +1,407 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! State-transition builder tests against the rs-dpp-generated fixtures in +//! `test_data/dpp_st_vectors.json` (Platform v4.0.0, protocol +//! version 12). The vectors carry full serialized transitions built from +//! deterministic inputs (fixed keys, entropy, RFC6979 ECDSA), so the +//! builders are checked byte-for-byte. +//! +//! The signing callback stands in for the C++ WalletSigner: it receives the +//! key id plus the double-SHA256 digest of the signable bytes and answers +//! with `dashcore::signer::sign_hash`, which is exactly what +//! `signer::sign(signable_bytes, key)` would produce. + +use std::sync::Mutex; + +use dash_platform_cxx::st::{self, AssetLockProofInput, NewIdentityKey, ASSET_LOCK_KEY_ID}; +use dash_platform_cxx::types::KeyInfo; +use dpp::dashcore::hashes::{sha256, Hash}; +use dpp::dashcore::signer as dash_signer; +use serde_json::Value; + +fn hexv(hex: &str) -> Vec { + hex::decode(hex).expect("bad hex in test vector") +} + +fn hex32(hex: &str) -> [u8; 32] { + hexv(hex).try_into().expect("expected 32 bytes") +} + +fn vectors() -> Value { + let doc: Value = serde_json::from_str(include_str!("../test_data/dpp_st_vectors.json")) + .expect("parse dpp_st_vectors.json"); + assert_eq!(doc["platform_repo_tag"].as_str(), Some("v4.0.0")); + doc +} + +/// Test signer: routes key ids to the fixed vector keys and records every +/// digest it was asked to sign. +struct TestSigner { + master_sk: [u8; 32], + high_sk: [u8; 32], + asset_lock_sk: [u8; 32], + digests: Mutex>, +} + +impl TestSigner { + fn from_vectors(doc: &Value) -> Self { + TestSigner { + master_sk: hex32(doc["keys"]["master"]["private_key_hex"].as_str().unwrap()), + high_sk: hex32(doc["keys"]["high"]["private_key_hex"].as_str().unwrap()), + asset_lock_sk: hex32( + doc["keys"]["asset_lock"]["private_key_hex"] + .as_str() + .unwrap(), + ), + digests: Mutex::new(Vec::new()), + } + } + + fn sign(&self, key_id: u32, digest: [u8; 32]) -> Option> { + self.digests.lock().unwrap().push((key_id, digest)); + let sk = match key_id { + 0 => self.master_sk, + 1 => self.high_sk, + ASSET_LOCK_KEY_ID => self.asset_lock_sk, + _ => return None, + }; + dash_signer::sign_hash(&digest, &sk) + .ok() + .map(|s| s.to_vec()) + } +} + +fn high_key(doc: &Value) -> KeyInfo { + KeyInfo { + id: 1, + purpose: 0, // AUTHENTICATION + security_level: 2, // HIGH + key_type: 0, // ECDSA_SECP256K1 + read_only: false, + data: hexv(doc["keys"]["high"]["public_key_hex"].as_str().unwrap()), + disabled_at: None, + } +} + +fn check_built( + built: &dash_platform_cxx::types::BuiltTransition, + vector: &Value, + signer: &TestSigner, + expected_key_id: u32, +) { + assert_eq!( + hex::encode(&built.bytes), + vector["serialized_hex"].as_str().unwrap(), + "serialized transition bytes" + ); + assert_eq!( + built.hash, + sha256::Hash::hash(&built.bytes).to_byte_array(), + "hash must be single sha256 of the bytes" + ); + let expected_digest = hex32(vector["digest_hex"].as_str().unwrap()); + let digests = signer.digests.lock().unwrap(); + assert!( + digests.contains(&(expected_key_id, expected_digest)), + "signer must be asked for the vector digest with key id {expected_key_id}; got {digests:?}" + ); +} + +#[test] +fn dpns_preorder() { + let doc = vectors(); + let vector = &doc["dpns_preorder"]; + let signer = TestSigner::from_vectors(&doc); + let built = st::build_dpns_preorder( + &hexv(vector["owner_id_hex"].as_str().unwrap()), + vector["identity_contract_nonce"].as_u64().unwrap(), + vector["label"].as_str().unwrap(), + &hexv(vector["salt_hex"].as_str().unwrap()), + &high_key(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build DPNS preorder"); + check_built(&built, vector, &signer, 1); +} + +fn build_domain( + doc: &Value, + vector: &Value, + signer: &TestSigner, +) -> dash_platform_cxx::types::BuiltTransition { + st::build_dpns_domain( + &hexv(vector["owner_id_hex"].as_str().unwrap()), + vector["identity_contract_nonce"].as_u64().unwrap(), + vector["label"].as_str().unwrap(), + vector["normalized_label"].as_str().unwrap(), + "dash", + &hexv(vector["salt_hex"].as_str().unwrap()), + &high_key(doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build DPNS domain") +} + +#[test] +fn dpns_domain() { + let doc = vectors(); + let vector = &doc["dpns_domain"]; + assert_eq!(vector["contested"].as_bool(), Some(false)); + let signer = TestSigner::from_vectors(&doc); + let built = build_domain(&doc, vector, &signer); + check_built(&built, vector, &signer, 1); +} + +// A contested label must automatically attach the prefunded voting balance +// (rs-dpp computes it from the contested unique index of the domain type). +#[test] +fn dpns_domain_contested() { + let doc = vectors(); + let vector = &doc["dpns_domain_contested"]; + assert_eq!(vector["contested"].as_bool(), Some(true)); + let signer = TestSigner::from_vectors(&doc); + let built = build_domain(&doc, vector, &signer); + check_built(&built, vector, &signer, 1); +} + +#[test] +fn dpns_domain_rejects_bad_normalization() { + let doc = vectors(); + let signer = TestSigner::from_vectors(&doc); + let err = st::build_dpns_domain( + &[0x11; 32], + 1, + "Alice", + "alice", // wrong: o->0, l/i->1 normalization gives "a11ce" + "dash", + &[0x55; 32], + &high_key(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .unwrap_err(); + assert!(err.contains("normalized label"), "{err}"); +} + +#[test] +fn dashpay_profile_create() { + let doc = vectors(); + let vector = &doc["dashpay_profile_create"]; + let signer = TestSigner::from_vectors(&doc); + let built = st::build_profile( + &hexv(vector["owner_id_hex"].as_str().unwrap()), + vector["identity_contract_nonce"].as_u64().unwrap(), + vector["display_name"].as_str().unwrap(), + vector["public_message"].as_str().unwrap(), + vector["avatar_url"].as_str().unwrap(), + &hexv(vector["avatar_hash_hex"].as_str().unwrap()), + &hexv(vector["avatar_fingerprint_hex"].as_str().unwrap()), + 1, + None, + &hexv(vector["entropy_hex"].as_str().unwrap()), + &high_key(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build profile create"); + check_built(&built, vector, &signer, 1); +} + +#[test] +fn dashpay_profile_replace() { + let doc = vectors(); + let create = &doc["dashpay_profile_create"]; + let vector = &doc["dashpay_profile_replace"]; + let signer = TestSigner::from_vectors(&doc); + // The replace fixture carries the create fixture's profile fields with + // an updated publicMessage (see the serialized property map). + let built = st::build_profile( + &hexv(vector["owner_id_hex"].as_str().unwrap()), + vector["identity_contract_nonce"].as_u64().unwrap(), + create["display_name"].as_str().unwrap(), + vector["public_message"].as_str().unwrap(), + create["avatar_url"].as_str().unwrap(), + &hexv(create["avatar_hash_hex"].as_str().unwrap()), + &hexv(create["avatar_fingerprint_hex"].as_str().unwrap()), + vector["revision"].as_u64().unwrap(), + Some(&hexv(vector["document_id_hex"].as_str().unwrap())), + &[0u8; 32], // entropy is unused for replacements + &high_key(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build profile replace"); + check_built(&built, vector, &signer, 1); +} + +#[test] +fn dashpay_contact_request() { + let doc = vectors(); + let vector = &doc["dashpay_contact_request"]; + let signer = TestSigner::from_vectors(&doc); + let built = st::build_contact_request( + &hexv(vector["owner_id_hex"].as_str().unwrap()), + vector["identity_contract_nonce"].as_u64().unwrap(), + &hexv(vector["to_user_id_hex"].as_str().unwrap()), + &hexv(vector["encrypted_public_key_hex"].as_str().unwrap()), + vector["sender_key_index"].as_u64().unwrap() as u32, + vector["recipient_key_index"].as_u64().unwrap() as u32, + vector["account_reference"].as_u64().unwrap() as u32, + &hexv(vector["encrypted_account_label_hex"].as_str().unwrap()), + &hexv(vector["entropy_hex"].as_str().unwrap()), + &high_key(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build contact request"); + check_built(&built, vector, &signer, 1); +} + +fn identity_keys(doc: &Value) -> Vec { + vec![ + NewIdentityKey { + id: 0, + purpose: 0, // AUTHENTICATION + security_level: 0, // MASTER + pubkey: hexv(doc["keys"]["master"]["public_key_hex"].as_str().unwrap()), + }, + NewIdentityKey { + id: 1, + purpose: 0, + security_level: 2, // HIGH + pubkey: hexv(doc["keys"]["high"]["public_key_hex"].as_str().unwrap()), + }, + ] +} + +fn check_identity_create( + built: &dash_platform_cxx::types::BuiltTransition, + vector: &Value, + signer: &TestSigner, +) { + assert_eq!( + hex::encode(&built.bytes), + vector["serialized_hex"].as_str().unwrap(), + "serialized identity create bytes" + ); + let expected_digest = hex32(vector["digest_hex"].as_str().unwrap()); + let digests = signer.digests.lock().unwrap().clone(); + // Every identity key and the asset-lock key sign the same digest. + for key_id in [0, 1, ASSET_LOCK_KEY_ID] { + assert!( + digests.contains(&(key_id, expected_digest)), + "key {key_id} must sign the vector digest" + ); + } + + // Round trip: the built bytes must deserialize back into an + // IdentityCreate transition with signatures present. + use dpp::serialization::PlatformDeserializable; + use dpp::state_transition::StateTransition; + let deserialized = + StateTransition::deserialize_from_bytes(&built.bytes).expect("round-trip deserialize"); + match &deserialized { + StateTransition::IdentityCreate(_) => {} + other => panic!("expected IdentityCreate, got {other:?}"), + } + let reserialized = { + use dpp::serialization::PlatformSerializable; + deserialized.serialize_to_bytes().expect("re-serialize") + }; + assert_eq!(reserialized, built.bytes, "round trip must be stable"); + assert!( + deserialized + .signature() + .map(|signature| !signature.is_empty()) + .unwrap_or(false), + "outer signature must be present" + ); +} + +#[test] +fn identity_create_instant() { + let doc = vectors(); + let vector = &doc["identity_create_instant"]; + let signer = TestSigner::from_vectors(&doc); + let built = st::build_identity_create( + AssetLockProofInput::Instant { + transaction: hexv(vector["transaction_hex"].as_str().unwrap()), + instant_lock: hexv(vector["instant_lock_hex"].as_str().unwrap()), + output_index: vector["output_index"].as_u64().unwrap() as u32, + }, + &identity_keys(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build identity create (instant)"); + check_identity_create(&built, vector, &signer); +} + +#[test] +fn identity_create_chain() { + let doc = vectors(); + let vector = &doc["identity_create_chain"]; + let signer = TestSigner::from_vectors(&doc); + let built = st::build_identity_create( + AssetLockProofInput::Chain { + core_chain_locked_height: vector["core_chain_locked_height"].as_u64().unwrap() as u32, + out_point: hexv(vector["out_point_hex"].as_str().unwrap()) + .try_into() + .expect("36-byte outpoint"), + }, + &identity_keys(&doc), + &|key_id, digest| signer.sign(key_id, digest), + ) + .expect("build identity create (chain)"); + check_identity_create(&built, vector, &signer); +} + +// Keys must arrive at the wire sorted by id with no duplicates, regardless +// of input order. +#[test] +fn identity_create_sorts_and_rejects_duplicate_keys() { + let doc = vectors(); + let vector = &doc["identity_create_chain"]; + let signer = TestSigner::from_vectors(&doc); + let proof = || AssetLockProofInput::Chain { + core_chain_locked_height: vector["core_chain_locked_height"].as_u64().unwrap() as u32, + out_point: hexv(vector["out_point_hex"].as_str().unwrap()) + .try_into() + .unwrap(), + }; + + let mut reversed = identity_keys(&doc); + reversed.reverse(); + let built = st::build_identity_create(proof(), &reversed, &|key_id, digest| { + signer.sign(key_id, digest) + }) + .expect("reversed key order still builds"); + assert_eq!( + hex::encode(&built.bytes), + vector["serialized_hex"].as_str().unwrap() + ); + + let mut duplicated = identity_keys(&doc); + duplicated[1].id = 0; + let err = st::build_identity_create(proof(), &duplicated, &|key_id, digest| { + signer.sign(key_id, digest) + }) + .unwrap_err(); + assert!(err.contains("duplicate"), "{err}"); +} + +#[test] +fn signer_failure_is_reported() { + let doc = vectors(); + let vector = &doc["dpns_preorder"]; + let err = st::build_dpns_preorder( + &hexv(vector["owner_id_hex"].as_str().unwrap()), + 2, + vector["label"].as_str().unwrap(), + &hexv(vector["salt_hex"].as_str().unwrap()), + &high_key(&doc), + &|_key_id, _digest| None, // wallet refuses + ) + .unwrap_err(); + assert!( + err.contains("signing failed") || err.contains("locked"), + "{err}" + ); +}