Skip to content
Open
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
147 changes: 75 additions & 72 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ base64 = "0.23.1"
# and `DisplayFromStr` adapters — the vulnerable `KeyValueMap` serializer is never invoked in the
# dependency chain, so the advisory is not exploitable here (Dependabot alert #41; dismissal as
# `not_used` was attempted but requires manual GitHub action — see coordinator note).
dash-sdk = { git = "https://github.com/dashpay/platform", rev = "575d961fe0c54bd7e01a3cf3823494e58c6c4fdb", features = [
dash-sdk = { git = "https://github.com/dashpay/platform", rev = "6323a430c731a14bbec1d7a18466ca28bb083316", features = [
"core_key_wallet",
"core_key_wallet_manager",
"core_bincode",
Expand All @@ -34,15 +34,15 @@ dash-sdk = { git = "https://github.com/dashpay/platform", rev = "575d961fe0c54bd
"core_spv",
"shielded",
] }
rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "575d961fe0c54bd7e01a3cf3823494e58c6c4fdb" }
platform-wallet = { git = "https://github.com/dashpay/platform", rev = "575d961fe0c54bd7e01a3cf3823494e58c6c4fdb", features = [
rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "6323a430c731a14bbec1d7a18466ca28bb083316" }
platform-wallet = { git = "https://github.com/dashpay/platform", rev = "6323a430c731a14bbec1d7a18466ca28bb083316", features = [
"serde",
"shielded",
] }
# `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 schemas are included by `secrets`.
platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "575d961fe0c54bd7e01a3cf3823494e58c6c4fdb", features = [
platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "6323a430c731a14bbec1d7a18466ca28bb083316", features = [
"shielded",
"serde",
] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#
# fingerprint = sha256 of the LC_ALL=C-sorted "<sha256> <path>" listing of every
# file under packages/rs-platform-wallet-storage/migrations at that rev.
pin=575d961fe0c54bd7e01a3cf3823494e58c6c4fdb
pin=6323a430c731a14bbec1d7a18466ca28bb083316
target_branch=v5.1-dev
migration_count=19
fingerprint=79465f56868e4dfd6e0ba05e4eefe7e7d4ac8ef478c2579ba019d12f868278e6
75 changes: 71 additions & 4 deletions src/backend_task/contested_names/query_ending_times.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl AppContext {
}

/// What one page of the end-date index says about DPNS contests.
#[derive(Debug, Default)]
#[derive(Debug, Default, PartialEq, Eq)]
struct EndTimesPage {
/// Contested names and when their contests end.
names: BTreeMap<String, TimestampMillis>,
Expand All @@ -102,8 +102,9 @@ struct EndTimesPage {
polls: usize,
}

/// Read a page of the end-date index. The index holds the contests of every contract,
/// so only polls on the DPNS `domain` document type name a contested username.
/// Read a page of the end-date index. The index holds every vote poll kind and the
/// contests of every contract, so only contests on the DPNS `domain` document type name
/// a contested username; the rest count only towards `polls` and `last_end_time`.
fn dpns_end_times_page(
page: impl IntoIterator<Item = (TimestampMillis, Vec<VotePoll>)>,
dpns_contract_id: Identifier,
Expand All @@ -118,7 +119,10 @@ fn dpns_end_times_page(
document_type_name,
index_values,
..
}) = vote_poll;
}) = vote_poll
else {
continue;
};
if contract_id != dpns_contract_id || document_type_name != "domain" {
continue;
}
Expand Down Expand Up @@ -170,6 +174,9 @@ fn next_page_start(
mod tests {
use super::*;
use dash_sdk::dpp::platform_value::Value;
use dash_sdk::dpp::voting::vote_polls::yes_no_vote_poll::{
YesNoMinimumVotingPower, YesNoVotePoll,
};
use std::collections::BTreeSet;

fn contest(contract_id: Identifier, document_type: &str, name: &str) -> VotePoll {
Expand All @@ -184,6 +191,66 @@ mod tests {
})
}

const DPNS_CONTRACT: Identifier = Identifier::new([1; 32]);

fn yes_no_poll() -> VotePoll {
VotePoll::YesNoVotePoll(YesNoVotePoll {
resource_path: vec![vec![1u8, 2, 3].into()],
supermajority_numerator: 2,
supermajority_denominator: 3,
minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 },
})
}

fn dpns_contest(name: &str) -> VotePoll {
contest(DPNS_CONTRACT, "domain", name)
}

#[test]
fn end_times_page_keeps_dpns_contests_and_skips_yes_no_polls() {
let page = vec![
(100, vec![dpns_contest("alice"), yes_no_poll()]),
(200, vec![dpns_contest("bob")]),
];

let result = dpns_end_times_page(page, DPNS_CONTRACT);

assert_eq!(
result.names,
BTreeMap::from([("alice".to_string(), 100), ("bob".to_string(), 200)])
);
assert_eq!(result.last_end_time, Some(200));
}

#[test]
fn end_times_page_of_only_yes_no_polls_still_advances_the_walk() {
let page = vec![
(100, vec![dpns_contest("alice")]),
(300, vec![yes_no_poll()]),
];

let result = dpns_end_times_page(page, DPNS_CONTRACT);

assert_eq!(result.names, BTreeMap::from([("alice".to_string(), 100)]));
assert_eq!(
result.last_end_time,
Some(300),
"the next page must start after the yes/no poll, not after alice"
);

let only_yes_no = dpns_end_times_page(vec![(400, vec![yes_no_poll()])], DPNS_CONTRACT);
assert!(only_yes_no.names.is_empty());
assert_eq!(only_yes_no.last_end_time, Some(400));
}

#[test]
fn empty_end_times_page_ends_the_walk() {
assert_eq!(
dpns_end_times_page(Vec::new(), DPNS_CONTRACT),
EndTimesPage::default()
);
}

#[test]
fn only_dpns_domain_polls_name_a_contest() {
let dpns = Identifier::from([1; 32]);
Expand Down
78 changes: 78 additions & 0 deletions src/backend_task/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1972,6 +1972,16 @@ pub enum TaskError {
source_error: Box<SdkError>,
},

/// The request needs a Platform feature the network does not serve yet
/// (SdkError::ProtocolVersionNotActive); the SDK refused it without sending it.
#[error(
"This feature is not active on the network yet. Try again later, after the network upgrade."
)]
PlatformFeatureNotActive {
#[source]
source_error: Box<SdkError>,
},

/// Platform rejected the request (StateTransitionBroadcastError, unclassified cause).
#[error("The platform rejected this request. Please check your input and try again.")]
PlatformRejected {
Expand Down Expand Up @@ -4787,6 +4797,18 @@ impl From<SdkError> for TaskError {
SdkError::StaleNode(_) => TaskError::DapiStaleNode {
source_error: boxed,
},
// The SDK could not learn the network's version for a reason that
// can pass, so the feature may be active already: not an upgrade wait.
SdkError::ProtocolVersionNotActive { .. }
if boxed.is_transient_protocol_version_refusal() =>
{
TaskError::DapiUnavailable {
source_error: boxed,
}
}
SdkError::ProtocolVersionNotActive { .. } => TaskError::PlatformFeatureNotActive {
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved
source_error: boxed,
},
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved
SdkError::NoAvailableAddressesToRetry(_) => TaskError::DapiAllAddressesExhausted {
source: std::sync::Arc::new(TaskError::SdkError {
source_error: boxed,
Expand Down Expand Up @@ -5692,6 +5714,62 @@ mod tests {
}
}

fn protocol_version_not_active(cause: Option<Box<SdkError>>) -> SdkError {
SdkError::ProtocolVersionNotActive {
method: "get_contract_fee_pots",
required: 15,
network: 14,
pinned: false,
cause,
}
}

#[test]
fn from_sdk_error_protocol_version_not_active_maps_to_platform_feature_not_active() {
// The cause is this SDK's own setup: retrying cannot change the refusal.
let permanent_cause = Box::new(SdkError::ContextProviderError(
dash_sdk::error::ContextProviderError::Config("no quorum source".to_string()),
));
for sdk_err in [
protocol_version_not_active(None),
protocol_version_not_active(Some(permanent_cause)),
] {
let err = TaskError::from(sdk_err);
match &err {
TaskError::PlatformFeatureNotActive { source_error } => assert!(
matches!(**source_error, SdkError::ProtocolVersionNotActive { .. }),
"source not preserved: {source_error:?}"
),
other => panic!("expected PlatformFeatureNotActive, got {other:?}"),
}
}
}

#[test]
fn from_sdk_error_transient_protocol_version_refusal_maps_to_dapi_unavailable() {
let sdk_err = protocol_version_not_active(Some(Box::new(SdkError::DapiClientError(
DapiClientError::NoAvailableAddresses,
))));
assert!(sdk_err.is_transient_protocol_version_refusal());
match TaskError::from(sdk_err) {
TaskError::DapiUnavailable { source_error } => assert!(
matches!(*source_error, SdkError::ProtocolVersionNotActive { .. }),
"source not preserved: {source_error:?}"
),
other => panic!("expected DapiUnavailable, got {other:?}"),
}
}

#[test]
fn platform_feature_not_active_display_is_user_friendly() {
let msg = TaskError::from(protocol_version_not_active(None)).to_string();
assert!(msg.contains("Try again"), "no action in: {msg}");
let lower = msg.to_lowercase();
for jargon in ["protocol", "sdk", "version", "get_contract_fee_pots", "15"] {
assert!(!lower.contains(jargon), "jargon {jargon:?} in: {msg}");
}
}

#[test]
fn from_sdk_error_missing_signing_key_maps_to_no_withdrawal_signing_key() {
let sdk_err = SdkError::Protocol(
Expand Down
17 changes: 6 additions & 11 deletions src/backend_task/platform_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,15 +1217,13 @@ mod tests {
assert_eq!(text, "summary", "a version this build defines");
}

/// The advice reaches the user even when the epoch itself cannot be read.
/// A failed epoch fetch still answers, with the report of an unavailable
/// epoch: the network's version and the last known fee multiplier.
#[tokio::test]
async fn a_failed_epoch_fetch_still_advises_updating_for_a_newer_network() {
use crate::context::test_support::prove_protocol_version;
use dash_sdk::dpp::version::LATEST_VERSION;
async fn a_failed_epoch_fetch_degrades_to_the_unavailable_epoch_report() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ctx = crate::context::test_support::test_app_context(temp_dir.path());
ctx.sdk.store(Arc::new(dash_sdk::Sdk::new_mock()));
prove_protocol_version(&ctx, LATEST_VERSION + 1);
let sdk = ctx.sdk.load_full();

let result = ctx
Expand All @@ -1238,12 +1236,9 @@ mod tests {
else {
panic!("a failed epoch fetch returns a text result");
};
assert!(
text.ends_with(&expected_newer_protocol_advisory(
LATEST_VERSION + 1,
LATEST_VERSION
)),
"the update advice must close the report, got: {text}"
assert_eq!(
text,
format_unavailable_current_epoch_info(None, ctx.fee_multiplier_permille())
);
}

Expand Down
56 changes: 36 additions & 20 deletions src/backend_task/shielded/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ mod tests {
use crate::context::test_support::test_app_context;
use crate::model::user_role::UserRole;
use crate::utils::egui_mpsc::SenderAsync;
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
use dash_sdk::platform::DataContract;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_style_direct_dispatch_rejects_unavailable_shielded_write() {
Expand Down Expand Up @@ -466,24 +468,38 @@ mod tests {
(ctx, backend)
}

/// Tell `sdk` the network runs protocol `version`, the way a response
/// from the network does.
fn network_reports_protocol_version(sdk: &dash_sdk::Sdk, version: u32) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_millis() as u64;
sdk.verify_response_metadata(
"test",
&dash_sdk::dapi_grpc::platform::v0::ResponseMetadata {
height: 1,
time_ms: now_ms,
protocol_version: version,
..Default::default()
},
)
.expect("fresh metadata");
assert_eq!(sdk.protocol_version_number(), version);
/// A context whose wallet backend runs on a mock connection at protocol 13
/// that answers the DPNS-contract fetch, so a test can move the backend's
/// version mid-transfer ([`network_raises_protocol_version`]).
async fn ctx_with_mock_backend(
dir: &std::path::Path,
) -> (Arc<AppContext>, Arc<crate::wallet_backend::WalletBackend>) {
let ctx = test_app_context(dir);
let mut sdk = crate::context::test_support::mock_sdk_at_for(&ctx, 13);
let dpns = ctx.dpns_contract();
sdk.mock()
.expect_fetch(dpns.id(), Some(DataContract::clone(&dpns)))
.await
.expect("register DPNS contract response");
ctx.sdk.store(Arc::new(sdk));
let (tx, _rx) = tokio::sync::mpsc::channel::<TaskResult>(32);
ctx.ensure_wallet_backend(SenderAsync::new(tx, egui::Context::default()))
.await
.expect("wallet backend");
let backend = ctx.wallet_backend().expect("wallet backend");
assert_eq!(backend.sdk().protocol_version_number(), 13);
(ctx, backend)
}

/// Have the network move `sdk` past protocol 13, the way a response from
/// the network does: a fetch answered by the mock raises the version the
/// SDK builds transitions at (to the newest this build defines).
async fn network_raises_protocol_version(ctx: &AppContext, sdk: &dash_sdk::Sdk) {
use dash_sdk::platform::Fetch;
DataContract::fetch(sdk, ctx.dpns_contract().id())
.await
.expect("mocked DPNS contract fetch");
assert!(sdk.protocol_version_number() > 13, "the version moved");
}

/// The version moves while the transfer is under way, and the transfer
Expand All @@ -493,13 +509,13 @@ mod tests {
async fn shield_from_core_task_succeeds_without_an_amount_when_the_version_moves_mid_transfer()
{
let tmp = tempfile::tempdir().expect("tempdir");
let (ctx, backend) = ctx_with_backend(tmp.path()).await;
let (ctx, backend) = ctx_with_mock_backend(tmp.path()).await;
let seed_hash = WalletSeedHash::default();

let result = ctx
.shield_from_core_with(seed_hash, 100_000, async |funding| {
assert_eq!(funding.lock_duffs, 100_000 + 212_852);
network_reports_protocol_version(backend.sdk(), 14);
network_raises_protocol_version(&ctx, backend.sdk()).await;
Ok(())
})
.await;
Expand Down
Loading
Loading