From b81bb242605c9eaff38d723b8063bbdc61bd079c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 20 Jul 2026 21:14:18 +0800 Subject: [PATCH 01/12] fix(drive): bound distinct sum range queries --- .../drive_dispatcher.rs | 62 +++++++++++++++++++ .../execute_range_sum.rs | 18 +++--- .../src/query/drive_document_sum_query/mod.rs | 3 + 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs index 90ca4fbbb92..b4dbc9dcb4d 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs @@ -22,6 +22,29 @@ use dpp::platform_value::Value; use dpp::version::PlatformVersion; use grovedb::TransactionArg; +fn effective_no_proof_distinct_limit( + requested_limit: Option, + drive_config: &crate::config::DriveConfig, +) -> Result { + let effective_limit = requested_limit + .unwrap_or(drive_config.default_query_limit as u32) + .min(drive_config.max_query_limit as u32); + + if effective_limit == 0 { + return Err(Error::Query( + crate::error::query::QuerySyntaxError::InvalidLimit( + "effective distinct SUM limit must be greater than zero".to_string(), + ), + )); + } + + u16::try_from(effective_limit).map_err(|_| { + Error::Query(crate::error::query::QuerySyntaxError::InvalidLimit( + "effective distinct SUM limit does not fit u16".to_string(), + )) + }) +} + #[cfg(feature = "server")] impl Drive { /// Server-side entry point for the sum surface. Routes a @@ -66,6 +89,7 @@ impl Drive { DocumentSumMode::PerInValue => { let options = RangeSumOptions { return_distinct_sums_in_range: false, + distinct_limit: None, carrier_outer_limit: None, left_to_right: order_by_ascending, }; @@ -87,8 +111,12 @@ impl Drive { request.mode, SumMode::GroupByRange | SumMode::GroupByCompound ); + let distinct_limit = return_distinct + .then(|| effective_no_proof_distinct_limit(request.limit, request.drive_config)) + .transpose()?; let options = RangeSumOptions { return_distinct_sums_in_range: return_distinct, + distinct_limit, carrier_outer_limit: None, left_to_right: order_by_ascending, }; @@ -252,3 +280,37 @@ pub fn where_clauses_from_value(value: &Value) -> Result, Error pub fn order_clauses_from_value(value: &Value) -> Result, Error> { crate::query::drive_document_count_query::drive_dispatcher::order_clauses_from_value(value) } + +#[cfg(test)] +mod tests { + use super::effective_no_proof_distinct_limit; + use crate::config::DriveConfig; + + #[test] + fn no_proof_distinct_limit_uses_the_default_and_clamps_to_the_maximum() { + let config = DriveConfig { + default_query_limit: 25, + max_query_limit: 100, + ..DriveConfig::default() + }; + + assert_eq!( + effective_no_proof_distinct_limit(None, &config).unwrap(), + 25 + ); + assert_eq!( + effective_no_proof_distinct_limit(Some(7), &config).unwrap(), + 7 + ); + assert_eq!( + effective_no_proof_distinct_limit(Some(10_000), &config).unwrap(), + 100 + ); + + let disabled = DriveConfig { + max_query_limit: 0, + ..config + }; + assert!(effective_no_proof_distinct_limit(None, &disabled).is_err()); + } +} diff --git a/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs b/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs index 2696fd519f7..b6e1cd71a8e 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs @@ -148,13 +148,17 @@ impl DriveDocumentSumQuery<'_> { }]); } - // Distinct mode. Mirror count's analog; currently relies on - // `distinct_sum_path_query` which is stubbed (pending port). - // Defer to the same builder so the error surfaces cleanly when - // distinct mode is requested before the builder body lands. - let (path_query_limit, left_to_right) = (None::, options.left_to_right); - let path_query = - self.distinct_sum_path_query(path_query_limit, left_to_right, platform_version)?; + // Distinct mode must always be bounded before the storage walk. + let distinct_limit = options.distinct_limit.ok_or_else(|| { + Error::Query(QuerySyntaxError::InvalidLimit( + "distinct range SUM execution requires an effective limit".to_string(), + )) + })?; + let path_query = self.distinct_sum_path_query( + Some(distinct_limit), + options.left_to_right, + platform_version, + )?; let base_path_len = path_query.path.len(); let mut drive_operations = vec![]; diff --git a/packages/rs-drive/src/query/drive_document_sum_query/mod.rs b/packages/rs-drive/src/query/drive_document_sum_query/mod.rs index 8b48a5ab122..05138eb600f 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/mod.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/mod.rs @@ -269,6 +269,9 @@ pub struct RangeSumOptions { /// When `true`, emit one `SumEntry` per distinct in-range value /// rather than a single `Aggregate(i64)`. pub return_distinct_sums_in_range: bool, + /// `Some(n)` caps the distinct range walk before GroveDB materializes + /// matching entries. `None` is valid only for non-distinct execution. + pub distinct_limit: Option, /// `Some(n)` caps the carrier walk for compound `(In, range)` /// shapes at n entries. `None` accepts the platform-wide /// `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`. From 25f9b5e071bb89d3b4ab314ca7091d364e55759c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 21 Jul 2026 13:59:24 +0800 Subject: [PATCH 02/12] test(drive): cover bounded distinct sums --- .../drive_dispatcher.rs | 8 +- .../query/drive_document_sum_query/tests.rs | 113 +++++++++++++++++- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs index b4dbc9dcb4d..40218ddac77 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs @@ -38,11 +38,9 @@ fn effective_no_proof_distinct_limit( )); } - u16::try_from(effective_limit).map_err(|_| { - Error::Query(crate::error::query::QuerySyntaxError::InvalidLimit( - "effective distinct SUM limit does not fit u16".to_string(), - )) - }) + // Both configuration limits are u16, and the `min` above bounds every + // caller-supplied value to `max_query_limit` before this conversion. + Ok(effective_limit as u16) } #[cfg(feature = "server")] diff --git a/packages/rs-drive/src/query/drive_document_sum_query/tests.rs b/packages/rs-drive/src/query/drive_document_sum_query/tests.rs index 46a7deb18bc..7383238b8c5 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/tests.rs @@ -255,7 +255,7 @@ mod limit_policy_regression { use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::drive_document_sum_query::{ - DocumentSumRequest, DocumentSumResponse, DriveDocumentSumQuery, SumMode, + DocumentSumRequest, DocumentSumResponse, DriveDocumentSumQuery, RangeSumOptions, SumMode, }; use crate::query::{WhereClause, WhereOperator}; use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; @@ -557,4 +557,115 @@ mod limit_policy_regression { "error must name the rejected limit; got: {msg}" ); } + + #[test] + fn range_distinct_sum_no_proof_applies_default_explicit_and_max_limits() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let data_contract = build_widget_contract(); + drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("apply contract"); + + for (i, (color, amount)) in [("blue", 2u64), ("green", 3), ("red", 5), ("yellow", 7)] + .iter() + .enumerate() + { + insert_widget(&drive, &data_contract, i, color, *amount); + } + + let document_type = data_contract + .document_type_for_name("widget") + .expect("widget"); + let drive_config = DriveConfig { + default_query_limit: 2, + max_query_limit: 3, + ..Default::default() + }; + let make_request = |limit| DocumentSumRequest { + contract: &data_contract, + document_type, + sum_property: "amount".to_string(), + where_clauses: vec![WhereClause { + field: "color".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("blue".to_string()), + }], + order_clauses: Vec::new(), + mode: SumMode::GroupByRange, + limit, + prove: false, + drive_config: &drive_config, + }; + + for (requested, expected) in [(None, 2), (Some(1), 1), (Some(10_000), 3)] { + let response = drive + .execute_document_sum_request(make_request(requested), None, platform_version) + .expect("bounded no-proof distinct SUM should succeed"); + let entries = match response { + DocumentSumResponse::Entries(entries) => entries, + other => panic!("expected Entries response, got {other:?}"), + }; + assert_eq!( + entries.len(), + expected, + "unexpected entry count for requested limit {requested:?}" + ); + } + } + + #[test] + fn distinct_sum_executor_rejects_missing_effective_limit_before_storage() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let data_contract = build_widget_contract(); + let document_type = data_contract + .document_type_for_name("widget") + .expect("widget"); + let where_clauses = vec![WhereClause { + field: "color".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("blue".to_string()), + }]; + let index = crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses( + document_type.indexes(), + &where_clauses, + "amount", + ) + .expect("byColor rangeSummable index"); + let query = DriveDocumentSumQuery { + document_type, + contract_id: data_contract.id().to_buffer(), + document_type_name: "widget".to_string(), + index, + where_clauses, + sum_property: "amount".to_string(), + }; + + let err = query + .execute_range_sum_no_proof( + &drive, + &RangeSumOptions { + return_distinct_sums_in_range: true, + distinct_limit: None, + carrier_outer_limit: None, + left_to_right: true, + }, + None, + platform_version, + ) + .expect_err("distinct execution must reject a missing effective limit"); + + assert!( + matches!(err, Error::Query(QuerySyntaxError::InvalidLimit(_))), + "expected QuerySyntaxError::InvalidLimit, got {err:?}" + ); + } } From ee3c9af5d6709de597d50f4879bd3dccb27e26b7 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 21 Jul 2026 14:25:47 +0800 Subject: [PATCH 03/12] refactor(drive): type bounded range sum walks --- .../drive_dispatcher.rs | 4 +- .../drive_dispatcher.rs | 20 +++++--- .../execute_range_sum.rs | 13 ++--- .../executors/range_no_proof.rs | 2 +- .../src/query/drive_document_sum_query/mod.rs | 19 ++++--- .../query/drive_document_sum_query/tests.rs | 50 +------------------ packages/rs-drive/src/query/mod.rs | 6 ++- 7 files changed, 38 insertions(+), 76 deletions(-) diff --git a/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs index 4810e4ea973..526a5c86b57 100644 --- a/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_average_query/drive_dispatcher.rs @@ -1322,7 +1322,7 @@ mod tests { /// Flat-summed range cross-check: `Aggregate + range` on a PCPS /// index resolves to `DocumentSumMode::RangeNoProof` with - /// `return_distinct_sums_in_range = false`. The joint executor + /// `walk_mode = RangeSumWalkMode::Aggregate`. The joint executor /// folds visited PCPS elements via `count_sum_value_or_default()` /// in Rust (no engine-side combined accumulator exists). Pin parity /// vs. the independent count + sum aggregate dispatch — this is the @@ -1409,7 +1409,7 @@ mod tests { /// Compound-summed range cross-check: `GroupByIn + In + range` on /// a PCPS index resolves to `DocumentSumMode::RangeNoProof` with - /// `return_distinct_sums_in_range = false`. The joint executor's + /// `walk_mode = RangeSumWalkMode::Aggregate`. The joint executor's /// distinct path query expresses the multi-In outer walk as a /// single grovedb call (atomicity inherent) and folds each /// In-branch's PCPS elements into one `(count, sum)` pair via diff --git a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs index 40218ddac77..68ce1b2aad9 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs @@ -13,7 +13,8 @@ use crate::drive::Drive; use crate::error::Error; use crate::query::drive_document_sum_query::{ - DocumentSumMode, DocumentSumRequest, DocumentSumResponse, RangeSumOptions, SumMode, + DocumentSumMode, DocumentSumRequest, DocumentSumResponse, RangeSumOptions, RangeSumWalkMode, + SumMode, }; use crate::query::{OrderClause, WhereClause}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -86,8 +87,7 @@ impl Drive { } DocumentSumMode::PerInValue => { let options = RangeSumOptions { - return_distinct_sums_in_range: false, - distinct_limit: None, + walk_mode: RangeSumWalkMode::Aggregate, carrier_outer_limit: None, left_to_right: order_by_ascending, }; @@ -109,12 +109,16 @@ impl Drive { request.mode, SumMode::GroupByRange | SumMode::GroupByCompound ); - let distinct_limit = return_distinct - .then(|| effective_no_proof_distinct_limit(request.limit, request.drive_config)) - .transpose()?; + let walk_mode = if return_distinct { + RangeSumWalkMode::Distinct(effective_no_proof_distinct_limit( + request.limit, + request.drive_config, + )?) + } else { + RangeSumWalkMode::Aggregate + }; let options = RangeSumOptions { - return_distinct_sums_in_range: return_distinct, - distinct_limit, + walk_mode, carrier_outer_limit: None, left_to_right: order_by_ascending, }; diff --git a/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs b/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs index b6e1cd71a8e..baeb52c1c3d 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/execute_range_sum.rs @@ -12,7 +12,7 @@ //! regular range proof against the `ProvableSumTree`, returning //! per-key `KVSum` ops bound to the merk root. -use super::{DriveDocumentSumQuery, RangeSumOptions, SumEntry}; +use super::{DriveDocumentSumQuery, RangeSumOptions, RangeSumWalkMode, SumEntry}; use crate::drive::Drive; use crate::error::query::QuerySyntaxError; use crate::error::Error; @@ -50,7 +50,7 @@ impl DriveDocumentSumQuery<'_> { .iter() .any(|wc| wc.operator == WhereOperator::In); - if !options.return_distinct_sums_in_range { + if matches!(options.walk_mode, RangeSumWalkMode::Aggregate) { if has_in_on_prefix { // Enforce exactly one `In` clause. Without this, a request // with multiple In filters would silently use only the @@ -148,12 +148,9 @@ impl DriveDocumentSumQuery<'_> { }]); } - // Distinct mode must always be bounded before the storage walk. - let distinct_limit = options.distinct_limit.ok_or_else(|| { - Error::Query(QuerySyntaxError::InvalidLimit( - "distinct range SUM execution requires an effective limit".to_string(), - )) - })?; + let RangeSumWalkMode::Distinct(distinct_limit) = options.walk_mode else { + unreachable!("aggregate range sums return before the distinct storage walk") + }; let path_query = self.distinct_sum_path_query( Some(distinct_limit), options.left_to_right, diff --git a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs index 2eada0792e9..be1ad9c44f7 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/executors/range_no_proof.rs @@ -15,7 +15,7 @@ use grovedb::TransactionArg; impl Drive { /// Range-sum walk against a `rangeSummable: true` index. Returns /// a summed entry or per-distinct-value entries depending on - /// `options.return_distinct_sums_in_range`. + /// `options.walk_mode`. #[allow(clippy::too_many_arguments)] pub fn execute_document_sum_range_no_proof( &self, diff --git a/packages/rs-drive/src/query/drive_document_sum_query/mod.rs b/packages/rs-drive/src/query/drive_document_sum_query/mod.rs index 05138eb600f..c1fb5d6fe6b 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/mod.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/mod.rs @@ -261,17 +261,24 @@ pub struct DriveDocumentSumQuery<'a> { pub sum_property: String, } +/// Storage-walk shape for a server-side range sum. +#[cfg(feature = "server")] +#[derive(Clone, Copy, Debug, Default)] +pub enum RangeSumWalkMode { + /// Return one aggregate sum for the range. + #[default] + Aggregate, + /// Return distinct sums, bounded by the supplied storage-walk limit. + Distinct(u16), +} + /// Server-side range-sum executor options, parallels /// [`crate::query::drive_document_count_query::RangeCountOptions`]. #[cfg(feature = "server")] #[derive(Clone, Debug, Default)] pub struct RangeSumOptions { - /// When `true`, emit one `SumEntry` per distinct in-range value - /// rather than a single `Aggregate(i64)`. - pub return_distinct_sums_in_range: bool, - /// `Some(n)` caps the distinct range walk before GroveDB materializes - /// matching entries. `None` is valid only for non-distinct execution. - pub distinct_limit: Option, + /// Select aggregate execution or a compile-time bounded distinct walk. + pub walk_mode: RangeSumWalkMode, /// `Some(n)` caps the carrier walk for compound `(In, range)` /// shapes at n entries. `None` accepts the platform-wide /// `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`. diff --git a/packages/rs-drive/src/query/drive_document_sum_query/tests.rs b/packages/rs-drive/src/query/drive_document_sum_query/tests.rs index 7383238b8c5..6af4716b682 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/tests.rs @@ -255,7 +255,7 @@ mod limit_policy_regression { use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::query::drive_document_sum_query::{ - DocumentSumRequest, DocumentSumResponse, DriveDocumentSumQuery, RangeSumOptions, SumMode, + DocumentSumRequest, DocumentSumResponse, DriveDocumentSumQuery, SumMode, }; use crate::query::{WhereClause, WhereOperator}; use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; @@ -620,52 +620,4 @@ mod limit_policy_regression { ); } } - - #[test] - fn distinct_sum_executor_rejects_missing_effective_limit_before_storage() { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); - let data_contract = build_widget_contract(); - let document_type = data_contract - .document_type_for_name("widget") - .expect("widget"); - let where_clauses = vec![WhereClause { - field: "color".to_string(), - operator: WhereOperator::GreaterThan, - value: Value::Text("blue".to_string()), - }]; - let index = crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses( - document_type.indexes(), - &where_clauses, - "amount", - ) - .expect("byColor rangeSummable index"); - let query = DriveDocumentSumQuery { - document_type, - contract_id: data_contract.id().to_buffer(), - document_type_name: "widget".to_string(), - index, - where_clauses, - sum_property: "amount".to_string(), - }; - - let err = query - .execute_range_sum_no_proof( - &drive, - &RangeSumOptions { - return_distinct_sums_in_range: true, - distinct_limit: None, - carrier_outer_limit: None, - left_to_right: true, - }, - None, - platform_version, - ) - .expect_err("distinct execution must reject a missing effective limit"); - - assert!( - matches!(err, Error::Query(QuerySyntaxError::InvalidLimit(_))), - "expected QuerySyntaxError::InvalidLimit, got {err:?}" - ); - } } diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index 21895f8e16a..2c3669f70e3 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -42,11 +42,13 @@ pub use drive_document_count_query::{ DocumentCountRequest, DocumentCountResponse, RangeCountOptions, MAX_LIMIT_AS_FAILSAFE, }; -// `DocumentSumRequest` / `DocumentSumResponse` / `RangeSumOptions` are +// `DocumentSumRequest` / `DocumentSumResponse` / range-sum options are // the server-side executor inputs and stay `server`-only (parallels // the count-side `DocumentCountRequest` etc. above). #[cfg(feature = "server")] -pub use drive_document_sum_query::{DocumentSumRequest, DocumentSumResponse, RangeSumOptions}; +pub use drive_document_sum_query::{ + DocumentSumRequest, DocumentSumResponse, RangeSumOptions, RangeSumWalkMode, +}; // `DocumentAverageRequest` / `DocumentAverageResponse` are the // server-side executor inputs for the average surface and stay From 9470096403ddb7d6c58d7f7a1a1096b798dd6475 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 21 Jul 2026 15:24:06 +0800 Subject: [PATCH 04/12] fix(ci): bound Swift SDK build disk usage --- .github/workflows/swift-sdk-build.yml | 4 ++ packages/swift-sdk/build_ios.sh | 55 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/.github/workflows/swift-sdk-build.yml b/.github/workflows/swift-sdk-build.yml index a6a2c06445b..55b73e71084 100644 --- a/.github/workflows/swift-sdk-build.yml +++ b/.github/workflows/swift-sdk-build.yml @@ -87,5 +87,9 @@ jobs: rm -rf packages/swift-sdk/SwiftExampleApp/.build || true - name: Build and test Swift SDK + env: + # The self-hosted runner persists Cargo's target cache between jobs. + # Keep only one Apple architecture's intermediates at a time. + PRUNE_CARGO_TARGETS: "1" run: | bash packages/swift-sdk/run_tests.sh diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh index 0cd72299a6f..59e3a77760b 100755 --- a/packages/swift-sdk/build_ios.sh +++ b/packages/swift-sdk/build_ios.sh @@ -24,6 +24,8 @@ TARGET_DIR="$ROOT_DIR/target" PACKAGE="rs-unified-sdk-ffi" XCFRAMEWORK="$SCRIPT_DIR/DashSDKFFI.xcframework" PROFILE="dev" +PRUNE_CARGO_TARGETS="${PRUNE_CARGO_TARGETS:-0}" +STAGING_DIR="" # Crates whose cbindgen-generated headers ship in the unified framework. # Order matters: earlier headers define types referenced by later ones. @@ -46,6 +48,31 @@ CLEAN=false log_info() { echo -e "${GREEN}$1${NC}"; } log_error() { echo -e "${RED}$1${NC}"; } +cleanup_staging_dir() { + if [ -n "$STAGING_DIR" ]; then + rm -rf "$STAGING_DIR" + fi +} + +stage_target_artifacts() { + local target="$1" + local library="$2" + local headers="$3" + local target_staging_dir="$STAGING_DIR/$target" + + mkdir -p "$target_staging_dir" + cp "$library" "$target_staging_dir/" + cp -R "$headers" "$target_staging_dir/include" + + STAGED_LIB="$target_staging_dir/$(basename "$library")" + STAGED_HEADERS="$target_staging_dir/include" + + # The final static library and generated headers are all xcodebuild needs. + # Release the much larger per-architecture dependency tree before building + # the next target so persistent CI runners cannot exhaust their disk. + rm -rf "$TARGET_DIR/$target" +} + # ------------------------------- # Help # ------------------------------- @@ -125,6 +152,19 @@ OUTPUT_DIR="$PROFILE" log_info "Package: $PACKAGE" log_info "Profile: $PROFILE" +if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dash-sdk-ffi.XXXXXX")" + trap cleanup_staging_dir EXIT + + # Persistent self-hosted runners may contain incomplete or obsolete builds + # from an earlier job. Start the bounded build with only Cargo's shared host + # cache, then prune each Apple target after staging its final artifacts. + rm -rf \ + "$TARGET_DIR/aarch64-apple-ios" \ + "$TARGET_DIR/aarch64-apple-ios-sim" \ + "$TARGET_DIR/aarch64-apple-darwin" +fi + # ------------------------------- # Build commands # ------------------------------- @@ -195,6 +235,11 @@ if $BUILD_IOS; then IOS_LIB="$TARGET_DIR/$IOS_TARGET/$OUTPUT_DIR/librs_unified_sdk_ffi.a" IOS_HEADERS="$TARGET_DIR/$IOS_TARGET/$OUTPUT_DIR/include" inject_modulemap "$IOS_HEADERS" + if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + stage_target_artifacts "$IOS_TARGET" "$IOS_LIB" "$IOS_HEADERS" + IOS_LIB="$STAGED_LIB" + IOS_HEADERS="$STAGED_HEADERS" + fi fi # iOS simulator @@ -205,6 +250,11 @@ if $BUILD_SIM; then SIM_LIB="$TARGET_DIR/$SIM_TARGET/$OUTPUT_DIR/librs_unified_sdk_ffi.a" SIM_HEADERS="$TARGET_DIR/$SIM_TARGET/$OUTPUT_DIR/include" inject_modulemap "$SIM_HEADERS" + if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + stage_target_artifacts "$SIM_TARGET" "$SIM_LIB" "$SIM_HEADERS" + SIM_LIB="$STAGED_LIB" + SIM_HEADERS="$STAGED_HEADERS" + fi fi # macOS @@ -215,6 +265,11 @@ if $BUILD_MAC; then MAC_LIB="$TARGET_DIR/$MAC_TARGET/$OUTPUT_DIR/librs_unified_sdk_ffi.a" MAC_HEADERS="$TARGET_DIR/$MAC_TARGET/$OUTPUT_DIR/include" inject_modulemap "$MAC_HEADERS" + if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + stage_target_artifacts "$MAC_TARGET" "$MAC_LIB" "$MAC_HEADERS" + MAC_LIB="$STAGED_LIB" + MAC_HEADERS="$STAGED_HEADERS" + fi fi # ------------------------------- From b7a3bd568cc4823c379bfdbb86eaec24cd5f6bce Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 03:43:30 +0800 Subject: [PATCH 05/12] fix(ci): guard Swift SDK cleanup paths --- packages/swift-sdk/build_ios.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh index 59e3a77760b..b858f846fb2 100755 --- a/packages/swift-sdk/build_ios.sh +++ b/packages/swift-sdk/build_ios.sh @@ -70,7 +70,7 @@ stage_target_artifacts() { # The final static library and generated headers are all xcodebuild needs. # Release the much larger per-architecture dependency tree before building # the next target so persistent CI runners cannot exhaust their disk. - rm -rf "$TARGET_DIR/$target" + rm -rf "${TARGET_DIR:?}/${target:?}" } # ------------------------------- @@ -132,7 +132,7 @@ done if $CLEAN; then log_info "Cleaning all build artifacts..." - rm -rf "$TARGET_DIR" + rm -rf "${TARGET_DIR:?}" rm -rf "$XCFRAMEWORK" fi @@ -160,9 +160,9 @@ if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then # from an earlier job. Start the bounded build with only Cargo's shared host # cache, then prune each Apple target after staging its final artifacts. rm -rf \ - "$TARGET_DIR/aarch64-apple-ios" \ - "$TARGET_DIR/aarch64-apple-ios-sim" \ - "$TARGET_DIR/aarch64-apple-darwin" + "${TARGET_DIR:?}/aarch64-apple-ios" \ + "${TARGET_DIR:?}/aarch64-apple-ios-sim" \ + "${TARGET_DIR:?}/aarch64-apple-darwin" fi # ------------------------------- From 88cd37c2b287165d2f67834d28411056d9a8e472 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 03:55:38 +0800 Subject: [PATCH 06/12] style(jni): format transaction decoder --- packages/rs-unified-sdk-jni/src/tx_decode.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/rs-unified-sdk-jni/src/tx_decode.rs b/packages/rs-unified-sdk-jni/src/tx_decode.rs index 3dc8cb840ad..02aeafc3f49 100644 --- a/packages/rs-unified-sdk-jni/src/tx_decode.rs +++ b/packages/rs-unified-sdk-jni/src/tx_decode.rs @@ -130,7 +130,13 @@ fn decode_to_blob(tx_bytes: &[u8], network: FFINetwork) -> Result, (i32, }; let mut out: *mut DecodedTransactionFFI = std::ptr::null_mut(); let ok = unsafe { - transaction_decode(tx_bytes.as_ptr(), tx_bytes.len(), network, &mut out, &mut error) + transaction_decode( + tx_bytes.as_ptr(), + tx_bytes.len(), + network, + &mut out, + &mut error, + ) }; if !ok || out.is_null() { let message = if error.message.is_null() { @@ -281,7 +287,10 @@ mod tests { let bytes = serialize(&tx); let blob = decode_to_blob(&bytes, FFINetwork::Testnet).expect("decode ok"); - let mut r = Reader { blob: &blob, pos: 0 }; + let mut r = Reader { + blob: &blob, + pos: 0, + }; assert_eq!(r.take(32), tx.txid().to_byte_array()); assert_eq!(r.u32(), 1, "one input"); @@ -321,7 +330,10 @@ mod tests { fn network_changes_rendered_addresses() { let (tx, addr) = p2pkh_spend_tx(Network::Testnet); let blob = decode_to_blob(&serialize(&tx), FFINetwork::Mainnet).expect("decode ok"); - let mut r = Reader { blob: &blob, pos: 0 }; + let mut r = Reader { + blob: &blob, + pos: 0, + }; r.take(32); r.u32(); r.take(36); From eafd192f7bd31e7ed50d0e54f0514160fd8a104e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 04:15:42 +0800 Subject: [PATCH 07/12] ci(kotlin): submit emulator unlock credential --- .github/workflows/kotlin-sdk-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/kotlin-sdk-build.yml b/.github/workflows/kotlin-sdk-build.yml index 809c1d7e983..9d235a82fb4 100644 --- a/.github/workflows/kotlin-sdk-build.yml +++ b/.github/workflows/kotlin-sdk-build.yml @@ -172,6 +172,8 @@ jobs: # stays unlocked for the whole run instead of re-locking. adb shell input keyevent KEYCODE_WAKEUP adb shell wm dismiss-keyguard + adb shell input text 1234 + adb shell input keyevent KEYCODE_ENTER sleep 5 # Fail loudly if the device is still locked, so a spurious From 275d8448391835d41497f82190d6357369fca22d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 05:03:15 +0800 Subject: [PATCH 08/12] ci(rust): tolerate transient coverage cleanup races --- .github/workflows/tests-rs-workspace.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 29bd4839d00..1fd3661df6f 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -238,7 +238,16 @@ jobs: run: | if [ -d target/llvm-cov-target ]; then du -sh target/llvm-cov-target || true - rm -rf target/llvm-cov-target + for attempt in 1 2 3; do + if rm -rf target/llvm-cov-target; then + break + fi + echo "::warning::Coverage cleanup attempt ${attempt} failed; retrying" + sleep 2 + done + if [ -d target/llvm-cov-target ]; then + echo "::warning::Coverage artifacts could not be fully removed" + fi fi du -sh target 2>/dev/null || true From 332d937eed6b8b19ba3a1d6f2aecc6dd050ff667 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 06:09:13 +0800 Subject: [PATCH 09/12] ci(rust): allow cold macOS workspace builds to finish --- .github/workflows/tests-rs-workspace.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 1fd3661df6f..cd661295adc 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -14,7 +14,7 @@ jobs: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository || github.event.pull_request.head.repo.owner.login == 'thepastaclaw' - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: Check out repo uses: actions/checkout@v4 From 8d1cf5b03ae3af4c2f567b34e3d8a0f9d11b8c11 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 07:15:16 +0800 Subject: [PATCH 10/12] ci(rust): cover shielded tests in macOS timeout --- .github/workflows/tests-rs-workspace.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index cd661295adc..d1232bc0d04 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -14,7 +14,7 @@ jobs: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository || github.event.pull_request.head.repo.owner.login == 'thepastaclaw' - timeout-minutes: 60 + timeout-minutes: 90 steps: - name: Check out repo uses: actions/checkout@v4 From 155e7a85b3f84c218aca93b1328a1eafce72254f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 09:04:11 +0800 Subject: [PATCH 11/12] ci(kotlin): retry emulator credential unlock --- .github/workflows/kotlin-sdk-build.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/kotlin-sdk-build.yml b/.github/workflows/kotlin-sdk-build.yml index 9d235a82fb4..66cdff7cba2 100644 --- a/.github/workflows/kotlin-sdk-build.yml +++ b/.github/workflows/kotlin-sdk-build.yml @@ -170,15 +170,10 @@ jobs: # Unlock the keyguard. With the screen kept on above, the device now # stays unlocked for the whole run instead of re-locking. - adb shell input keyevent KEYCODE_WAKEUP - adb shell wm dismiss-keyguard - adb shell input text 1234 - adb shell input keyevent KEYCODE_ENTER - sleep 5 - - # Fail loudly if the device is still locked, so a spurious - # InvalidKeyException can't masquerade as a real test failure. - adb shell dumpsys trust | grep -q 'deviceLocked=0' || { echo "::error::Emulator is still locked (deviceLocked=1); Keystore-backed tests would fail spuriously."; adb shell dumpsys trust; exit 1; } + # Credential acceptance and keyguard dismissal can race during a + # cold emulator boot. Retry the complete sequence atomically, then + # fail loudly before tests if the device never reaches unlocked. + for attempt in 1 2 3; do adb shell input keyevent KEYCODE_WAKEUP; adb shell wm dismiss-keyguard; adb shell input text 1234; adb shell input keyevent KEYCODE_ENTER; sleep 2; adb shell wm dismiss-keyguard; sleep 1; adb shell dumpsys trust | grep -q 'deviceLocked=0' && exit 0; done; echo "::error::Emulator is still locked (deviceLocked=1); Keystore-backed tests would fail spuriously."; adb shell dumpsys trust; exit 1 ./gradlew :sdk:connectedDebugAndroidTest --stacktrace From 8aaad75094b9e2a779aa9d48345688bb61c409c4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 10:37:29 +0800 Subject: [PATCH 12/12] ci(rust): allow cold macOS coverage runs --- .github/workflows/tests-rs-workspace.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index d1232bc0d04..cbf30cb5d6b 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -14,7 +14,7 @@ jobs: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository || github.event.pull_request.head.repo.owner.login == 'thepastaclaw' - timeout-minutes: 90 + timeout-minutes: 120 steps: - name: Check out repo uses: actions/checkout@v4