diff --git a/CHANGELOG.md b/CHANGELOG.md index fe59f32a3..d89df5e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Security + +- **Dependency advisory GHSA-4w2j-m93h-cj5j cleared**: the `quinn-proto` entry in + the lock file moves from 0.11.14 to 0.11.15, which fixes a remote + memory-exhaustion issue in out-of-order stream reassembly. The crate is an + inert optional entry that no build of this app actually links, so this is + dependency hygiene rather than a fix for reachable behavior. + +- **Dependency advisory GHSA-7gcf-g7xr-8hxj still open** (`serde_with` below + 3.21.0, a panic when serializing empty key-value map entries): it cannot be + resolved in this repository. `serde_with` 2.x is required by + `dashcore-rpc-json`, which arrives through pinned revisions of + `dashpay/platform` and `dashpay/rust-dashcore`; both still declare + `serde_with = "2.1.0"` at their current development heads as of 2026-07-27. + Allowing 3.x needs an upstream change in `dashpay/rust-dashcore` first. A TODO + in `Cargo.toml` marks the re-check. + ### Added - **Automatic Platform node refresh during upgrades**: migrating a pre-1.0 @@ -84,7 +101,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). the send-fee estimate from picking up the network's current rate. A temporary workaround is in place while the underlying issue is fixed upstream; the send-fee estimate will keep using its last known rate until - that lands. + that lands. The check only accepts a protocol version the connected network + actually confirms: when the network cannot be reached, shielded operations + stay unavailable and the app keeps retrying, instead of assuming the version + the app was built with. ### Changed diff --git a/Cargo.lock b/Cargo.lock index 13b516387..d82395f5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6968,9 +6968,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 269fba25a..c9140a835 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ qrcode = "0.14.1" nix = { version = "0.31.1", features = ["signal"] } 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 = "288a6cae4f9653d6085d2b3d6c7410210a0c95ba", features = [ "core_key_wallet", "core_key_wallet_manager", diff --git a/src/backend_task/platform_info.rs b/src/backend_task/platform_info.rs index e557fceda..5e2ae7b96 100644 --- a/src/backend_task/platform_info.rs +++ b/src/backend_task/platform_info.rs @@ -250,14 +250,23 @@ fn format_extended_epoch_info( ) } -fn format_unavailable_current_epoch_info(protocol_version: u32) -> String { - format!( - "Current Epoch Information:\n\ - • Protocol Version: {protocol_version}\n\ - • Epoch details and fee multiplier are temporarily unavailable while \ - dashpay/platform#4231 is unresolved.\n\n\ - (The fee multiplier cache was not updated.)" - ) +/// `protocol_version` is `None` while the connected network has not confirmed one. +fn format_unavailable_current_epoch_info(protocol_version: Option) -> String { + match protocol_version { + Some(protocol_version) => format!( + "Current Epoch Information:\n\ + • Protocol Version: {protocol_version}\n\ + • Epoch details and fee multiplier are temporarily unavailable while \ + dashpay/platform#4231 is unresolved.\n\n\ + (The fee multiplier cache was not updated.)" + ), + None => "Current Epoch Information:\n\ + • Protocol Version: the connected network has not confirmed one yet.\n\ + • Epoch details and fee multiplier are temporarily unavailable while \ + dashpay/platform#4231 is unresolved.\n\n\ + (The fee multiplier cache was not updated.)" + .to_string(), + } } fn format_current_quorums_info(current_quorums_info: &CurrentQuorumsInfo) -> String { @@ -470,23 +479,6 @@ fn withdrawal_status_str(status: WithdrawalStatus) -> &'static str { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn epoch_workaround_reports_protocol_version_and_stale_fee_cache() { - assert_eq!( - format_unavailable_current_epoch_info(12), - "Current Epoch Information:\n\ - • Protocol Version: 12\n\ - • Epoch details and fee multiplier are temporarily unavailable while \ - dashpay/platform#4231 is unresolved.\n\n\ - (The fee multiplier cache was not updated.)" - ); - } -} - /// Flatten one withdrawal [`Document`] into a [`WithdrawalRecord`]. fn extract_withdrawal_record( document: &Document, @@ -580,20 +572,23 @@ impl AppContext { )) } PlatformInfoTaskRequestType::CurrentEpochInfo => { - if let Err(error) = DataContract::fetch(sdk, self.dpns_contract.id()).await { - tracing::warn!( + // dashpay/platform#4231 breaks `ExtendedEpochInfo::fetch_current`, so the + // network's version is learned from the ratchet a proved DPNS fetch drives. + // Only a successful fetch proves it came from the network, not the local seed. + match DataContract::fetch(sdk, self.dpns_contract.id()).await { + Ok(_) => self.set_platform_protocol_version(sdk.protocol_version_number()), + Err(error) => tracing::warn!( %error, "Protocol-version ratchet trigger (DPNS contract fetch) failed; \ - keeping the SDK's previous protocol version" - ); + the network's protocol version stays unconfirmed" + ), } - let protocol_version = sdk.protocol_version_number(); - self.set_platform_protocol_version(protocol_version); match ExtendedEpochInfo::fetch_current(sdk).await { Ok(epoch_info) => { let fee_multiplier = epoch_info.fee_multiplier_permille(); self.set_fee_multiplier_permille(fee_multiplier); + self.set_platform_protocol_version(epoch_info.protocol_version()); let mut formatted = format_extended_epoch_info(epoch_info, self.network, true); @@ -611,9 +606,13 @@ impl AppContext { "Current epoch fetch is blocked by dashpay/platform#4231; \ keeping the cached fee multiplier" ); + let confirmed = match self.platform_protocol_version() { + 0 => None, + version => Some(version), + }; Ok(BackendTaskSuccessResult::PlatformInfo( PlatformInfoTaskResult::TextResult( - format_unavailable_current_epoch_info(protocol_version), + format_unavailable_current_epoch_info(confirmed), ), )) } @@ -934,3 +933,51 @@ impl AppContext { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epoch_workaround_reports_protocol_version_and_stale_fee_cache() { + assert_eq!( + format_unavailable_current_epoch_info(Some(12)), + "Current Epoch Information:\n\ + • Protocol Version: 12\n\ + • Epoch details and fee multiplier are temporarily unavailable while \ + dashpay/platform#4231 is unresolved.\n\n\ + (The fee multiplier cache was not updated.)" + ); + } + + #[test] + fn epoch_workaround_never_reports_an_unconfirmed_protocol_version_as_a_number() { + let formatted = format_unavailable_current_epoch_info(None); + assert!( + formatted + .contains("• Protocol Version: the connected network has not confirmed one yet."), + "an unconfirmed version must be named as such, got: {formatted}" + ); + } + + /// A failed ratchet trigger leaves the SDK reporting its local seed, which is + /// not a network observation: caching it would open the shielded capability + /// gate and defeat the `0` retry sentinel `mcp::resolve` polls. + #[tokio::test] + async fn a_failed_ratchet_trigger_leaves_the_protocol_version_unconfirmed() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + let sdk = dash_sdk::Sdk::new_mock(); + assert_ne!( + sdk.protocol_version_number(), + 0, + "precondition: the mock SDK seeds a local version of its own" + ); + + ctx.run_platform_info_task(PlatformInfoTaskRequestType::CurrentEpochInfo, &sdk) + .await + .expect("the epoch workaround degrades to a text result"); + + assert_eq!(ctx.platform_protocol_version(), 0); + } +}