Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
146 changes: 114 additions & 32 deletions src/backend_task/contested_names/query_ending_times.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,8 @@ impl AppContext {
order_ascending: true,
};

let new_end_times = match VotePoll::fetch_many(&sdk, end_time_query.clone()).await {
Ok(vote_polls) => {
let mut end_times = BTreeMap::new();
for (timestamp, vote_poll_list) in vote_polls {
let contests = vote_poll_list.into_iter().filter_map(|vote_poll| {
let VotePoll::ContestedDocumentResourceVotePoll(
ContestedDocumentResourceVotePoll {
contract_id: _,
document_type_name: _,
index_name: _,
index_values,
},
) = vote_poll;

index_values
.get(1)
.and_then(|a| a.to_str().ok().map(|a| (a.to_string(), timestamp)))
});
end_times.extend(contests);
}
end_times
}
let page = match VotePoll::fetch_many(&sdk, end_time_query.clone()).await {
Ok(vote_polls) => dpns_end_times_page(vote_polls),
Err(e) => {
tracing::error!("Error fetching vote polls: {}", e);
super::log_contested_proof_error(&e, RequestType::GetVotePollsByEndDate);
Expand All @@ -82,21 +62,123 @@ impl AppContext {
}
};

contests_end_times.extend(new_end_times.clone());

if new_end_times.is_empty() {
break;
}
contests_end_times.extend(page.names);

let last_found_ending_time = new_end_times.values().max();
if let Some(last_found_ending_time) = last_found_ending_time {
start_time = Some((*last_found_ending_time, false));
} else {
// Page on every poll returned, not only DPNS contests: a page holding
// only other polls must not end the walk.
let Some(last_end_time) = page.last_end_time else {
break;
}
};
start_time = Some((last_end_time, false));
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved
}

self.update_contested_name_ending_times(contests_end_times)?;
Ok(())
}
}

/// One page of the vote-poll end-date index.
#[derive(Debug, Default, PartialEq, Eq)]
struct EndTimesPage {
/// DPNS names whose contests end in this page, with their end time.
names: BTreeMap<String, u64>,
/// The latest end time of any poll in the page; `None` for an empty page.
last_end_time: Option<u64>,
}

/// Collect the DPNS contest end times from one page of the end-date index.
///
/// The index lists every vote poll kind, so the page can also hold polls that
/// are not DPNS contests (yes/no polls); they count only towards
/// `last_end_time`, which is where the next page starts.
fn dpns_end_times_page(page: impl IntoIterator<Item = (u64, Vec<VotePoll>)>) -> EndTimesPage {
let mut result = EndTimesPage::default();
for (timestamp, vote_polls) in page {
result.last_end_time = result.last_end_time.max(Some(timestamp));
for vote_poll in vote_polls {
let VotePoll::ContestedDocumentResourceVotePoll(ContestedDocumentResourceVotePoll {
index_values,
..
}) = vote_poll
else {
continue;
};
if let Some(name) = index_values.get(1).and_then(|value| value.to_str().ok()) {
result.names.insert(name.to_string(), timestamp);
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved
}
}
}
result
}
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Resolved

#[cfg(test)]
mod tests {
use super::*;
use dash_sdk::dpp::platform_value::Value;
use dash_sdk::dpp::voting::vote_polls::yes_no_vote_poll::{
YesNoMinimumVotingPower, YesNoVotePoll,
};

fn dpns_contest(name: &str) -> VotePoll {
VotePoll::ContestedDocumentResourceVotePoll(ContestedDocumentResourceVotePoll {
contract_id: Default::default(),
document_type_name: "domain".to_string(),
index_name: "parentNameAndLabel".to_string(),
index_values: vec![
Value::Text("dash".to_string()),
Value::Text(name.to_string()),
],
})
}

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 },
})
}

#[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);

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);

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()])]);
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()), EndTimesPage::default());
}
}
53 changes: 53 additions & 0 deletions src/backend_task/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1977,6 +1977,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 @@ -4792,6 +4802,9 @@ impl From<SdkError> for TaskError {
SdkError::StaleNode(_) => TaskError::DapiStaleNode {
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 @@ -5708,6 +5721,46 @@ 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() {
let transient_cause = Box::new(SdkError::DapiClientError(
DapiClientError::NoAvailableAddresses,
));
for sdk_err in [
protocol_version_not_active(None),
protocol_version_not_active(Some(transient_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 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