Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **Platform updated to `4.2.0-dev.8`** (`v4.2-dev`, `63cf57f`): existing
databases from the previously pinned PR are upgraded automatically with a
retained backup and verified data transfer. Both app preferences and network
wallet data are covered. Identity ownership changes preserve saved metadata,
and swept transactions leave the displayed history. The single-UTXO Max-send
regression test now passes and is enabled. See the
[upgrade review](docs/ai-design/2026-09-10-platform-pin/upgrade-notes.md) for
compatibility details and functionality still pending upstream.

- **A funding transaction found again on the network is now labelled honestly**:
when the app rediscovers a saved funding transaction from the chain rather
than tracking it from the start, it can tell that the network confirmed it but
Expand Down
232 changes: 124 additions & 108 deletions Cargo.lock

Large diffs are not rendered by default.

19 changes: 9 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ eframe = { version = "0.35.0", features = ["persistence", "wgpu"] }
base64 = "0.22.1"
# TODO: GHSA-7gcf-g7xr-8hxj (serde_with <3.21.0) is unfixable from here — the 2.x pin lives in
# dashcore-rpc-json (dashpay/rust-dashcore, rpc-json/Cargo.toml). Re-check when these pins move.
dash-sdk = { git = "https://github.com/dashpay/platform", rev = "67d4ef3f6340a1e983229b6870ef60cf7573602a", features = [
dash-sdk = { git = "https://github.com/dashpay/platform", rev = "63cf57f40d0000bf3b2b26026c8fa1c71162852d", features = [
"core_key_wallet",
"core_key_wallet_manager",
"core_bincode",
Expand All @@ -30,18 +30,17 @@ dash-sdk = { git = "https://github.com/dashpay/platform", rev = "67d4ef3f6340a1e
"core_spv",
"shielded",
] }
rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "67d4ef3f6340a1e983229b6870ef60cf7573602a" }
platform-wallet = { git = "https://github.com/dashpay/platform", rev = "67d4ef3f6340a1e983229b6870ef60cf7573602a", features = [
rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "63cf57f40d0000bf3b2b26026c8fa1c71162852d" }
platform-wallet = { git = "https://github.com/dashpay/platform", rev = "63cf57f40d0000bf3b2b26026c8fa1c71162852d", features = [
"serde",
"shielded",
] }
# `secret-serde` backs `model::secret::Secret`'s `Deserialize`: the upstream
# `serde` backs `model::secret::Secret`'s `Deserialize`: the upstream
# visitor copies a borrowed `&str` straight into guarded memory instead of
# routing through a transient `String`. `secret-schemars` is enabled by the
# `mcp`/`cli` features, which are the only ones that generate tool schemas.
platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "67d4ef3f6340a1e983229b6870ef60cf7573602a", features = [
# routing through a transient `String`. Secret schemas are included by `secrets`.
platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "63cf57f40d0000bf3b2b26026c8fa1c71162852d", features = [
"shielded",
"secret-serde",
"serde",
] }
zip32 = "0.2.0"
grovestark = { git = "https://www.github.com/dashpay/grovestark", rev = "5b9e289cca54c79b1305d5f4f40bf1148f1eb0e3" }
Expand Down Expand Up @@ -124,8 +123,8 @@ raw-cpuid = "11.5.0"
default = []
testing = []
bench = []
mcp = ["dep:rmcp", "rmcp/server", "rmcp/macros", "rmcp/transport-streamable-http-server", "dep:axum", "platform-wallet-storage/secret-schemars"]
cli = ["dep:rmcp", "rmcp/server", "rmcp/macros", "rmcp/client", "rmcp/transport-io", "rmcp/transport-streamable-http-client-reqwest", "dep:clap", "dep:clap_complete", "platform-wallet-storage/secret-schemars"]
mcp = ["dep:rmcp", "rmcp/server", "rmcp/macros", "rmcp/transport-streamable-http-server", "dep:axum"]
cli = ["dep:rmcp", "rmcp/server", "rmcp/macros", "rmcp/client", "rmcp/transport-io", "rmcp/transport-streamable-http-client-reqwest", "dep:clap", "dep:clap_complete"]
headless = ["cli", "mcp"]

[dev-dependencies]
Expand Down
91 changes: 91 additions & 0 deletions docs/ai-design/2026-09-10-platform-pin/upgrade-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Platform development pin upgrade

Reviewed on 2026-09-10. The four Platform dependencies move together from
`67d4ef3f6340a1e983229b6870ef60cf7573602a` (`4.2.0-dev.2`, PR #3968)
to `63cf57f40d0000bf3b2b26026c8fa1c71162852d` (`4.2.0-dev.8`, `v4.2-dev`).
The transitive rust-dashcore revision moves from `3d13d9838c80fb5e67cf1f62cf5f3f4477bd5b9a`
to `93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd`.

The selected revision includes [#4649](https://github.com/dashpay/platform/pull/4649),
which serializes pending contact-crypto persistence with identity removal.
Its changes relative to `e3cd7cf` leave public APIs, dependency manifests,
migration history and the database schema unchanged; the existing `e3cd7cf.sql`
schema guard therefore also applies to `63cf57f`.

## What survived the PR split

| Area | Result at the new pin |
| --- | --- |
| SQLite persistence and seedless rehydration | Included through [#3968](https://github.com/dashpay/platform/pull/3968), with a different migration lineage. |
| Typed persistence errors | Included through [#4586](https://github.com/dashpay/platform/pull/4586). Store retry eligibility is now an explicit backend contract. |
| Guarded editable secrets and password envelopes | Retained, including `SecretString::replace_range`; Argon2 working memory wiping improves. |
| Large operating-system memory pages | Secret storage now refuses page sizes above 16 KiB. Hosts using 64 KiB pages are not covered by the local Linux checks. |
| Secret deserialization/schema features | `secret-serde` becomes `serde`; schemas are included with `secrets`. |
| Provider-key reconstruction | Retained; the proposed FFI deduplication is not required by DET. |
| Contact-account scan coverage | [#4587](https://github.com/dashpay/platform/pull/4587) remains open. Upstream inserts contact accounts without invalidating `account_generation` and prior filter-scan coverage. DET does invalidate these for new accounts created at bootstrap/unlock; it cannot retroactively cover an account already inserted by recurring upstream sync. End-to-end contact-payment consequences still need network testing. |
| FFI asset-lock proof size gate | [#4585](https://github.com/dashpay/platform/pull/4585) remains open and the old gate is absent. DET consumes Rust wallet APIs, not this FFI entry point. |

## Compatibility work

- Document queries explicitly use an empty sub-query list, retaining their
existing single-query behavior.
- Persistence failures retain their typed source and use the new store-failure
classification; exhaustive wallet error handling includes new variants.
- Swept transactions are removed from DET's displayed transaction history while
retaining unrelated transactions and other wallets' history.
- The existing single-UTXO Max-send regression now passes; its test is enabled
in the ordinary test suite (DET #909 / rust-dashcore #911).
- [#4496](https://github.com/dashpay/platform/pull/4496) replaces identity
tombstones with hard deletion and metadata cascades. Ownership reconciliation
now preserves a still-listed identity until its wallet takes ownership,
including when that promotion must wait for a later reconciliation.
- Existing PR-pin databases use a compatibility bridge: their V001 checksum is
different and their V003 unified schema precedes the new branch's V009.
Rewriting migration checksums alone is invalid because the materialized schemas
also differ.

## Existing data

Both `det-app.sqlite` and the per-network wallet database use the bridge. It
recognizes the exact old migration history and materialized schema, retains a
SQLite backup beside the original (`<original-name>.platform-67d4ef3-backup-*.sqlite`), and
validates the converted data with the new storage reader before rebuilding the
original in one transaction. Unknown history, unexpected schema changes, or
unreadable data stop the upgrade rather than guessing at a conversion.

Shared wallet columns and opaque DET metadata are copied and compared byte for byte.
Version-domain aliases retain the largest sequence on a collision. A tombstoned
identity still present in DET's active roster remains live; genuinely retired
typed identity rows remain in the backup, while their opaque local metadata is
preserved. An ambiguous or malformed saved identity roster fails closed.
The encrypted seed vault is not migrated or rewritten.

Keep the retained backups. Downgrading does not automatically reverse the
database conversion; recovery requires the corresponding backup and the prior
application version. Validation uses synthetic upstream fixtures, not a user's
real profile.

## Validation and limits

- `cargo fmt --all`: completed.
- `cargo clippy --locked --all-features --all-targets -- -D warnings`: passed with the
exact CI flags.
- `cargo test --locked --lib --all-features`: 2520 passed, none failed or ignored.
This includes old app-preference and populated wallet upgrades, persisted
balances/identities, backup preservation, interrupted-process rollback,
WAL snapshots, writer exclusion, unknown-schema rejection and repeated open.
- Focused regressions reproduced and fixed stale swept-transaction history and
identity metadata deletion during ownership transfer. The single-UTXO Max-send
regression is included in the normal passing suite.
- Network-dependent backend E2E and GUI tests were not run. The checks used Linux
and synthetic data; they do not establish contact-payment behavior on a live
network or support for hosts with larger operating-system memory pages.

`cargo audit` reports the same five advisories against both lockfiles:
RUSTSEC-2026-0204 (`crossbeam-epoch`), RUSTSEC-2026-0258 (`h2`),
RUSTSEC-2026-0194 and RUSTSEC-2026-0195 (`quick-xml`), and RUSTSEC-2026-0257
(`webbrowser`). This upgrade introduces none of those five, but the audit is
not clean. Unmaintained/unsound/yanked warnings also remain. This is a bounded
upgrade review, not a whole-Platform security audit.

<sub>Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
1 change: 1 addition & 0 deletions src/backend_task/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ impl AppContext {
// Fetch the contract description from the Search Contract
let search_contract = &self.keyword_search_contract;
let document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: search_contract.clone(),
document_type_name: "fullDescription".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/backend_task/dashpay/contact_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ async fn resolve_username_to_identity(

// Use the cached DPNS contract from AppContext instead of fetching from network
let domain_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: app_context.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/backend_task/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl AppContext {
document_id: Identifier,
) -> Result<Document, TaskError> {
let document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract,
document_type_name: document_type.name().to_string(),
Expand Down
9 changes: 8 additions & 1 deletion src/backend_task/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ pub enum TaskError {
},

/// An identity is still in the wallet store's unowned scope immediately
/// after being withdrawn from it — upstream's tombstone write logs a
/// after being withdrawn from it — upstream's deletion write logs a
/// persist failure and reports the removal as done regardless, so the
/// readback is the only evidence it landed. The next boot's reconcile
/// re-issues the withdrawal, which is what the message offers. Carries the
Expand All @@ -423,6 +423,13 @@ pub enum TaskError {
source: platform_wallet_storage::WalletStorageError,
},

/// A pinned-PR wallet database could not be upgraded without losing data.
#[error(transparent)]
PlatformDatabaseUpgrade {
#[from]
source: crate::wallet_backend::platform_compatibility::UpgradeError,
},

/// Persisted Core transaction rows could not be read through the upstream
/// wallet persistence API during wallet registration.
#[error(
Expand Down
1 change: 1 addition & 0 deletions src/backend_task/identity/discover_identities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ impl AppContext {
use dash_sdk::platform::{Document, DocumentQuery, FetchMany};

let query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/backend_task/identity/load_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ impl AppContext {

// Fetch DPNS names using SDK
let dpns_names_document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions src/backend_task/identity/load_identity_by_dpns_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ impl AppContext {

// Query the DPNS contract for the domain document
let domain_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down Expand Up @@ -78,6 +79,7 @@ impl AppContext {

// Fetch all DPNS names owned by this identity
let dpns_names_document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/backend_task/identity/load_identity_from_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl AppContext {
let identity_id = identity.id();

let dpns_names_document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl AppContext {
let identity_id = qualified_identity.identity.id();

let dpns_names_document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/backend_task/identity/register_dpns_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ impl AppContext {
.map_err(|error| rebrand_dpns_domain_conflict(TaskError::from(error)))?;

let dpns_names_document_query = DocumentQuery {
sub_queries: Vec::new(),
select: SelectProjection::documents(),
data_contract: self.dpns_contract.clone(),
document_type_name: "domain".to_string(),
Expand Down
Loading
Loading