Repository navigation
feat(swift-sdk)!: use SPV-synced quorums for Platform proof verification - #3417
QuantumExplorer wants to merge 59 commits into
Conversation
Bridge the SPV client's locally synced masternode list data to the Platform SDK's context provider, replacing the dependency on a trusted HTTP quorum endpoint. When an SPV client handle is available, the SDK is created with callback-based quorum lookups that call through to ffi_dash_spv_get_quorum_public_key and ffi_dash_spv_get_platform_activation_height. A "Fallback to Trusted Quorums" toggle (default: ON) in Settings lets users fall back to the HTTP provider when SPV data is not yet synced. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough
Pre-merge checks:
|
|
🕓 Review not started yet because the new head is waiting for the 30-minute push debounce.
Commit 47e3355. Normal review starts when eligible; priority review starts as soon as a slot is available. |
|
✅ DashSDKFFI.xcframework built for this PR.
SwiftPM (host the zip at a stable URL, then use): .binaryTarget(
name: "DashSDKFFI",
url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
checksum: "93f085d7f665b623b4cc170feb6c8fc04da6702b601f887c52f8e5181e471994"
)Xcode manual integration:
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift`:
- Around line 45-49: The published property useTrustedQuorumFallback currently
only persists the flag; update its didSet to immediately reapply the SDK
proof-verification mode so the running app reflects the change. After
UserDefaults.standard.set(...), invoke the function that configures the SDK
trust mode (for example the AppState method that initializes or configures the
SDK client—if you have a method like applySDKMode(), reconfigureClient(), or
setTrustedQuorumFallback(on:)), passing the new useTrustedQuorumFallback value;
if no such function exists, call the code path that reconnects or reinitializes
the SDK client so proof verification is updated immediately. Ensure you
reference AppState and useTrustedQuorumFallback when wiring this change.
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift`:
- Around line 126-133: The current network-switch flow lets
walletService.switchNetwork(to:) destroy the SPV client while platformState.sdk
still holds callbacks to the old core handle; update
UnifiedAppState.handleNetworkSwitch to explicitly detach and destroy the old SDK
before tearing down the SPV client: capture the existing platformState.sdk (or
call a teardown method like
platformState.sdk?.destroy()/platformState.clearSDK()), set platformState.sdk =
nil (or call the SDK's close/unbind) on the MainActor to remove callbacks, then
call walletService.switchNetwork(to:) and only after obtaining
walletService.spvClientHandle recreate and assign the new SDK via
platformState.updateSPVClientHandle/new SDK creation — ensuring the old SDK is
fully unbound before SPV teardown.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ed48fb63-d5a9-41ff-b965-9df30eae9aab
📒 Files selected for processing (7)
packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swiftpackages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVContextProvider.swiftpackages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swiftpackages/swift-sdk/Sources/SwiftDashSDK/SDK.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
The quorum/activation-height bridging no longer round-trips through Swift callbacks. Instead, rs-sdk-ffi now depends on dash-spv-ffi directly and implements SpvContextProvider in Rust, calling ffi_dash_spv_get_quorum_public_key and ffi_dash_spv_get_platform_activation_height without leaving Rust. A new FFI function dash_sdk_create_with_spv_context(config, spv_client) replaces the Swift-side ContextProviderCallbacks setup. Swift just passes the raw FFIDashSpvClient pointer. - Add dash-spv-ffi dependency to rs-sdk-ffi - Add spv_context_provider.rs implementing ContextProvider trait - Add dash_sdk_create_with_spv_context FFI function - Remove SPVContextProvider.swift (no longer needed) - Update SDK.swift to call dash_sdk_create_with_spv_context Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## v5.1-dev #3417 +/- ##
=============================================
- Coverage 87.40% 64.77% -22.63%
=============================================
Files 2639 24 -2615
Lines 333066 2657 -330409
=============================================
- Hits 291103 1721 -289382
+ Misses 41963 936 -41027
🚀 New features to boost your workflow:
|
Add SpvContextProvider to platform-wallet behind the `spv-context` feature flag. It holds Arc<RwLock<MasternodeListEngine>> + Network and implements the ContextProvider trait by reading quorum data directly from the in-memory masternode list — zero FFI involvement. The rs-sdk-ffi bridge (which calls dash-spv-ffi functions as Rust calls in the same binary) remains as the pragmatic adapter until dash-spv-ffi exposes a public accessor for FFIDashSpvClient's MasternodeListEngine (currently pub(crate)). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/rs-platform-wallet/Cargo.toml`:
- Line 25: The tokio dependency must enable its sync feature for the spv-context
feature and the feature list must use the modern dep: syntax; update Cargo.toml
so the tokio dependency becomes optional with features = ["sync"] (e.g. tokio =
{ version = "1.41", optional = true, features = ["sync"] }) and update the
spv-context feature entry to include "dep:tokio/sync" (instead of the old name)
so spv_context_provider.rs can safely use tokio::sync::RwLock.
In `@packages/rs-platform-wallet/src/spv_context_provider.rs`:
- Around line 115-120: The match that sets `height` currently falls through to
`_ => 0`, silently mapping unsupported `self.network` to 0; replace that
fallback with an explicit error return (e.g., return Err(...) or propagate a
suitable error) so callers are notified of an unsupported Network variant.
Modify the block that computes `height` (the match on `self.network` referencing
`Network::Mainnet`, `Network::Testnet`, `Network::Devnet`) to return a
Result<u64, E> (or use the crate's existing error type) and produce a clear
error for the unsupported branch instead of using 0. Ensure callers of the
enclosing function are updated to handle the Result if necessary.
- Around line 79-80: The call to self.masternode_engine.blocking_read() inside
get_quorum_public_key (which is invoked from async
parse_proof_with_metadata_and_proof) can panic; replace blocking_read() with
try_read() on the Tokio RwLock (self.masternode_engine.try_read()) and handle
the None case gracefully by returning an appropriate error or early failure from
get_quorum_public_key (or falling back to a safe default) instead of unwrapping.
Ensure you reference and use the masternode_lists_around_height result from the
acquired guard when present, and propagate a clear error back to the async proof
verification path so it doesn’t panic.
- Around line 76-77: Validate and reject out-of-range quorum_type before
converting to LLMQType: replace the lossy cast "(quorum_type as u8).into()" with
a fallible conversion using TryInto (e.g., let llmq_type: LLMQType =
(quorum_type.try_into()?) or map the TryInto error to a meaningful error) so
values >255 are handled instead of wrapping; keep
QuorumHash::from_byte_array(quorum_hash) as-is and ensure any error returned
propagates to the caller before using ml.quorums.get(&llmq_type).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0b462740-73cb-43f3-9a1b-7653a611fe5e
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
packages/rs-platform-wallet/Cargo.tomlpackages/rs-platform-wallet/src/lib.rspackages/rs-platform-wallet/src/spv_context_provider.rspackages/rs-sdk-ffi/src/spv_context_provider.rs
✅ Files skipped from review due to trivial changes (1)
- packages/rs-platform-wallet/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/rs-sdk-ffi/src/spv_context_provider.rs
- AppState: trigger SDK reconnect when useTrustedQuorumFallback changes so the running app reflects the new setting immediately - UnifiedAppState: nil out old SDK before destroying SPV client during network switch to prevent use-after-free on the context provider's core_handle pointer - Cargo.toml: explicitly enable tokio "sync" feature for blocking_read() support - spv_context_provider.rs: replace lossy `as u8` cast with try_from + error, add comment explaining why blocking_read is safe, return error for unsupported networks instead of silent 0 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Bump rust-dashcore to 3f65002 which includes the FFIDashSpvClient::masternode_list_engine() accessor (PR #608). dash_sdk_create_with_spv_context now: 1. Casts the void* to FFIDashSpvClient 2. Extracts Arc<RwLock<MasternodeListEngine>> directly 3. Creates platform_wallet::SpvContextProvider (pure Rust) The FFI bridge module (rs-sdk-ffi/src/spv_context_provider.rs) is deleted — quorum lookups go directly from the Platform SDK through the pure-Rust provider to the in-memory masternode list, with zero FFI involvement in the data path. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
All review feedback addressed. Additionally, bumped rust-dashcore to |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift (1)
215-267:⚠️ Potential issue | 🟠 MajorMake the network switch commit atomically.
The new SPV-first failure paths can throw after the collections are cleared and
dataManager.currentNetworkis advanced, but beforesdkis replaced. On that path the UI says “new network” while platform calls still go through the old SDK instance. IfcurrentNetworkshould also be authoritative, it needs the same success-only commit behavior.♻️ Possible fix
func switchNetwork(to network: AppNetwork) async { guard let modelContext = modelContext else { return } - - // Clear current data - identities.removeAll() - contracts.removeAll() - documents.removeAll() - tokens.removeAll() - - // Update DataManager's current network - dataManager?.currentNetwork = network // Re-initialize SDK with new network do { isLoading = true @@ - sdk = newSDK + identities.removeAll() + contracts.removeAll() + documents.removeAll() + tokens.removeAll() + dataManager?.currentNetwork = network + sdk = newSDK🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift` around lines 215 - 267, The switchNetwork method mutates state (clearing identities/contracts/documents/tokens and advancing dataManager?.currentNetwork) before potentially failing during SDK creation and load; make the switch atomic by first creating and initializing the new SDK and loading contracts/data (use local temporaries like let newSDK and temp collections), and only after loadKnownContractsIntoSDK and loadPersistedData succeed assign sdk = newSDK, set dataManager?.currentNetwork = network, and replace identities/contracts/documents/tokens with the newly loaded data; keep all error-throwing work (SDK(network:), loadKnownContractsIntoSDK, loadPersistedData) before mutating shared state so failures leave the existing sdk and currentNetwork unchanged.
🧹 Nitpick comments (1)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift (1)
104-124: Extract the SPV/trusted SDK creation path into one helper.The same fallback tree now lives in both
initializeSDKandswitchNetwork. The next change to error handling or provider selection is easy to land in one path and miss in the other.Also applies to: 234-253
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift` around lines 104 - 124, Extract the SPV/trusted SDK creation logic used in initializeSDK and switchNetwork into a single helper function (e.g., makeSDKForNetwork(network:sdkNetwork, spvHandle:spvClientHandle?, useFallback:useTrustedQuorumFallback) -> SDK) that encapsulates the fallback tree: try constructing SDK(network: , spvClientHandle:) if spvHandle != nil, on failure fall back to SDK(network:) only if useFallback is true, otherwise rethrow; if spvHandle is nil create SDK(network:) when useFallback is true or throw SDKError.invalidState when false. Replace the duplicated blocks in initializeSDK and switchNetwork to call this new helper and preserve the same NSLog messages and error propagation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift`:
- Around line 143-146: The race occurs because updateSPVClientHandle(_:) runs in
handleNetworkSwitch() while switchNetwork(...) can be started asynchronously
from the currentNetwork didSet in OptionsView; modify handleNetworkSwitch() so
that after calling updateSPVClientHandle(_:) it immediately triggers/awaits the
SDK rebuild (call switchNetwork(...) directly or await the spawned Task) to
ensure the SDK is rebuilt with the new spvClientHandle before returning;
reference updateSPVClientHandle(_:), handleNetworkSwitch(), switchNetwork(...),
currentNetwork didSet/OptionsView to locate and change the code paths so the
handle update is always followed synchronously by the SDK rebuild.
---
Outside diff comments:
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift`:
- Around line 215-267: The switchNetwork method mutates state (clearing
identities/contracts/documents/tokens and advancing dataManager?.currentNetwork)
before potentially failing during SDK creation and load; make the switch atomic
by first creating and initializing the new SDK and loading contracts/data (use
local temporaries like let newSDK and temp collections), and only after
loadKnownContractsIntoSDK and loadPersistedData succeed assign sdk = newSDK, set
dataManager?.currentNetwork = network, and replace
identities/contracts/documents/tokens with the newly loaded data; keep all
error-throwing work (SDK(network:), loadKnownContractsIntoSDK,
loadPersistedData) before mutating shared state so failures leave the existing
sdk and currentNetwork unchanged.
---
Nitpick comments:
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift`:
- Around line 104-124: Extract the SPV/trusted SDK creation logic used in
initializeSDK and switchNetwork into a single helper function (e.g.,
makeSDKForNetwork(network:sdkNetwork, spvHandle:spvClientHandle?,
useFallback:useTrustedQuorumFallback) -> SDK) that encapsulates the fallback
tree: try constructing SDK(network: , spvClientHandle:) if spvHandle != nil, on
failure fall back to SDK(network:) only if useFallback is true, otherwise
rethrow; if spvHandle is nil create SDK(network:) when useFallback is true or
throw SDKError.invalidState when false. Replace the duplicated blocks in
initializeSDK and switchNetwork to call this new helper and preserve the same
NSLog messages and error propagation behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1d056420-6f71-49b1-ad49-3434618ac7b3
📒 Files selected for processing (4)
packages/rs-platform-wallet/Cargo.tomlpackages/rs-platform-wallet/src/spv_context_provider.rspackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/rs-platform-wallet/Cargo.toml
- packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift
- packages/rs-platform-wallet/src/spv_context_provider.rs
Remove the switchNetwork() call from currentNetwork.didSet to prevent it racing with UnifiedAppState.handleNetworkSwitch(). The didSet now only persists the preference. handleNetworkSwitch() is the single coordinator: it nils the old SDK, switches the wallet, updates the SPV handle, then calls switchNetwork() to rebuild the SDK. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Replace blocking_read() with try_read() in SpvContextProvider to avoid panicking when called from within a Tokio async context (proof verification runs inside async tasks) - Use dep: prefix syntax in spv-context feature to suppress implicit feature creation for optional dependencies (Cargo 2021 edition) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ovider Brings PR #3417 (SPV-synced Platform proof verification) up to date with v4.1-dev (~827 commits) and re-implements its obsolete integration surface against the restructured PlatformWalletManager architecture. Re-integration: - Delegate quorum lookups to SpvRuntime::get_quorum_public_key via a thin SpvContextProvider { Arc<SpvRuntime>, Network }, bridging the sync trait method to the async lookup with block_in_place + Handle::try_current. - New FFI entry platform_wallet_manager_create_sdk_with_spv_context in platform-wallet-ffi (rs-sdk-ffi cannot reach the manager handle without a dependency cycle). Make rs_sdk_ffi::ContextProviderWrapper public; delete the now-dead dash_sdk_create_with_spv_context and drop the dash-spv-ffi dep. - Swift: SDK.init(network:walletManager:) (@mainactor; captures the Sendable Handle before the nonisolated C-string closure); AppState builds the SDK SPV-first with trusted fallback; accept base's deletion of SPVClient / WalletService / UnifiedAppState. Correctness fixes: - Remove the erroneous `.reverse()` in SpvRuntime::get_quorum_public_key. quorum_hash is internal byte order end-to-end (drive-abci emits it via to_byte_array, proven by the consensus-equality check in finalize_block_proposal; the masternode engine keys its quorum map in the same order), so reversing missed every quorum -> fail-closed rejection, silently masked by the trusted-quorum fallback (SPV never actually used). Added a regression test pinning the byte order (internal-order hits, reversed misses). - Correct get_platform_activation_height to the shipped trusted-provider values (Mainnet 2_132_092, Testnet 1_090_319; were dash-spv-ffi "needs verification" placeholders). - Reclaim the ContextProviderWrapper box after dash_sdk_create_extended (which only borrows + Arc-clones it) to avoid leaking it per SDK creation. Merge breakages resolved: mergiraf-duplicated [workspace.dependencies] with conflicting rust-dashcore revs; platform-wallet-ffi <-> rs-sdk-ffi dependency cycle; deleted `manager` feature; redundant spv-context feature gating. Verified: 3 Rust crates compile + clippy-clean + fmt; byte-order regression test passes; SwiftExampleApp (simulator) BUILD SUCCEEDED; cbindgen header emits the new FFI function correctly. Not verified: end-to-end proof verification against live SPV-synced quorums (integration/on-device only). Co-Authored-By: Claude Opus 4.8 <[email protected]>
SDK.QuorumMode gains case docs: Auto uses SPV while it is synced and the trusted quorum service whenever it is not, which cutting off the SPV peers can force; .spv is the mode that never uses the trusted service. SwiftExampleApp's picker help said Auto stays on SPV once synced; it now says losing SPV falls back to trusted. Doc and help-text change only, so no test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The example's closing comment still showed Sdk::set_context_provider, which this branch removed; the provider is now given to SdkBuilder::with_context_provider. Comment-only change, so no test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…er tip as synced
dash-spv keeps masternode sync `Synced` when later list updates fail. A
restored masternode engine whose every QRInfo attempt times out still
completes its pipeline and turns `Synced` at the stored height. Its own
docs say the list is current only once `current_height()` reaches
`block_header_tip_height()`. `SpvRuntime::is_ready` checked the state
alone, so such a list counted as ready.
A ready source's miss maps to `ContextProviderError::Generic`, which the
SDK charges to the responding node, so every honest node signed by a
quorum mined after the stale list was banned in turn. In Auto, the stale
source also kept lookups off the trusted provider.
Readiness now also needs the newest masternode list at most two blocks
behind the header tip (`MASTERNODE_LIST_TIP_LAG`). A tip diff in flight
leaves readiness alone. A list further behind makes the source not
ready: Spv mode reports `QuorumSourceUnavailable`, which bans nobody, and
Auto answers from the trusted provider until the list catches up.
Test would have caught this in CI:
should_not_be_ready_while_the_masternode_list_trails_the_header_tip
✖ before the fix ("a list behind the header tip must not be ready"), ✔
after.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The book's SdkBuilder and Sdk listings still showed the provider in a Box and an ArcSwapOption, which allowed swapping it after build. It is now an Arc fixed when build() runs, which also refuses a provider for another network or one that requires proofs while they are off. The app-connect comment in the shared system-contract loader no longer mentions a fallback provider: the loader returns None, and only the trusted provider has a fallback. Docs and a comment only, so no test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-11-09T16:04:30.220Z |
|
Your move: thepastaclaw requested changes on this head; dismiss the review or push a fix; thepastaclaw left review threads unresolved; resolve them. |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified head 1ab26d6: 13 prior findings are fixed, and the SPV lease-placement objection is withdrawn. Two blocking manager-replacement defects remain—restoring before old persistence writers are quiescent and publishing an empty manager after restoration fails—plus one upstream readiness-tracking suggestion. This was a static review; current Rust workspace, wasm32, Swift and Kotlin CI checks pass, PR Hygiene remains pending, and the supplied base-drift analysis reports no merge conflicts.
🔴 2 blocking | 🟡 1 suggestion(s)
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The large, intricate change directly changes quorum public-key sourcing for cryptographic proof verification in packages/rs-context-provider/src/routing.rs and packages/rs-platform-wallet/src/spv_context_provider.rs, including trust-source routing and SPV eligibility checks. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/WalletManagerStore.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/WalletManagerStore.swift:297-307: Do not publish an empty replacement after wallet restoration fails
This catch converts an unavailable wallet snapshot into a successful empty replacement. The caller has already shut down the stale manager and unconditionally caches and publishes the new manager after this helper returns. Restoration genuinely can fail: the Swift persistence handler returns an error when fetching persisted wallets fails, and Rust's loader propagates that error and rolls back partial wallet registrations. The fresh manager can consequently remain empty while the SDK rebuild reports success.
This is not equivalent to a successful load containing no wallets. Existing wallets disappear from the active manager, and subsequent `activate` calls with the same SDK take the identity fast path without retrying hydration. The newly ordered handover therefore depends on a fallible operation that its comment incorrectly treats as unable to fail.
Carry restoration failure through the replacement lifecycle and expose an unavailable, retryable state rather than publishing an empty manager as successfully restored. Account for the fact that the old manager is already terminal: merely throwing into the current rollback path would leave the app retaining a shut-down manager. Add a failed-read/retry handover test.
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/WalletManagerStore.swift:262-265: Do not replace a manager after a non-clean shutdown
(existing thread: https://github.com/dashpay/platform/pull/3417#discussion_r4416654088)
Awaiting `stale.shutdown()` does not establish that its persistence writers have stopped. Rust's `PlatformWalletManager::shutdown` bounds coordinator drains and worker joins and explicitly reports timeouts with potentially live workers, but `platform_wallet_manager_destroy` removes the handle, logs that report and returns Success. Swift's shutdown metrics record those FFI results, not the worker-level report, so even inspecting the existing metrics would not establish quiescence.
The next statements load and publish another manager against the same network's persisted wallets. An already-admitted coordinator pass can finish after its drain budget and persist through its retained callback context; for example, `persistAddressBalances` directly assigns balance, nonce and `lastSeenHeight`. The replacement can therefore load before the old pass's final commit, or an old write can overwrite state subsequently written by the replacement. Callback ownership prevents dangling pointers, but does not serialize these writers.
Provide a wallet-owned replacement/quiescence contract that prevents hydration until old writers have terminated, or fences every superseded persistence writer, and expose its actionable outcome through FFI. Preserve the early configuration-failure behavior, and add coverage where an admitted worker outlives shutdown's budget; the current activation test only establishes that native destroy returned before the swap.
In `packages/rs-platform-wallet/src/spv/runtime.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/spv/runtime.rs:65-75: Track the stale-sync readiness repair in dash-spv
The two-block tolerance is a legitimate Platform policy, but the need to supplement `Synced` reflects an upstream health-reporting gap. At both the pinned and current upstream revisions, `MasternodesManager::settle_diff` returns from a given-up tip update without withdrawing `Synced`, and `SyncProgress::is_synced` checks phase states without freshness. This guard protects this provider, while other dash-spv consumers still receive that misleading health result.
Track the freshness/degradation contract in dashpay/rust-dashcore's masternode progress API and link the follow-up from this PR before deferring it. dashpay/rust-dashcore#954 covers related initial QRInfo retry exhaustion, but its stated scope does not cover failed incremental tip updates; extend that issue or link a dedicated one. Platform can retain its allowed-lag policy while consuming authoritative upstream freshness information.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Consume upstream validation-before-fork-recovery fix — The pinned dash-spv header pipeline can emit ForkDetected for an unmatched header before checking its proof of work or required difficulty, allowing an invalid fork announcement to trigger recovery. This is an existing upstream synchronization-availability defect, separate from the repaired quorum-list authentication path.
- Follow-up: Track dashpay/rust-dashcore#1304 and consume its validation-before-routing fix in a separate dependency-update PR.
… the persisted state A worker that `shutdown()` could not stop may still write the manager's persisted state through the host's callbacks, so no other manager may open that state until the process exits. The rustdoc said only that the host must keep its callback context alive. No test: documentation only. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…stroy `platform_wallet_manager_destroy` logged a non-clean `ShutdownReport` and returned `Success`. Memory safety did not need more, since stragglers own their callback contexts. But a straggler can still write the manager's persisted state, and a host that hands that state to a successor manager had no way to know. Destroy now returns `ErrorShutdownIncomplete`, with the report in the message, when `all_clean()` is false. "Clean" is still decided by `PlatformWalletManager::shutdown()`; the FFI maps the verdict to the existing code, as `shielded_sync_stop` does for an undrained pass. The handle is removed either way, so this one code 27 must not be retried, and the docs say so. The FFI crate depends on `dash-async` to name `ShutdownReport`. The docs that said destroy never returns code 27 or returns `Success` without proving quiescence are updated. Red -> green: `destroy_result_tests` was first run against the mapping extracted unchanged from 1ab26d6. The `Timeout` worker and detached orphan cases failed (left: Success, right: ErrorShutdownIncomplete), the clean case passed. With the fix all three pass. A test that drives a real straggler through destroy is not possible here: paused tokio time needs a current-thread runtime, and `ThreadRegistry::shutdown` asserts a multi-thread one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Destroy now returns `ErrorShutdownIncomplete` with the shutdown report in its message when a worker did not stop. The JNI destroy logged only the code, as "destroy failed". It now logs the message, which names the straggling workers. The Kotlin store does not act on the verdict yet. No test: log text only. Co-Authored-By: Claude Opus 5.5 <[email protected]>
`PlatformWalletShutdownMetrics.allWorkersStopped` is true only when the destroy step returned `Success`, or when there was no native manager to tear down. It marshals destroy's verdict. A host checks it before another manager opens the persisted state this one used: when it is false, a straggling worker may still write that state until the process exits. The property is computed, so the public init is unchanged. `configure` now frees the result of the destroy it runs after a failed capability query, which can carry a message now. Tests: destroy faked to return code 27 gives `allWorkersStopped == false`. Only the early shielded stop returning 27 still gives true, because destroy's own shutdown re-checks every worker. No observed red: the tests read the property this commit adds, and Swift tests run only in GitHub CI (no Xcode on the dev VM). Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e workers live A replaced manager whose native destroy reports a worker it could not stop must not be followed by a replacement that loads the network's wallets, because the straggler may still write them. The test fakes that report on a real regtest manager after its real destroy. It expects `activate` to throw an error that tells the user to restart the app. It also expects the shut-down manager to stay cached and active, the refused replacement to be shut down (its SDK's quorum source slot is free again), and a later rebuild to be refused before anything is configured. Red on its own: `activate` ignores the shutdown verdict, so it swaps in the replacement. The fix follows in the next commit. Swift tests run only in GitHub CI, so CI on this commit is the red run. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Your move: thepastaclaw requested changes on this head; dismiss the review or push a fix; thepastaclaw left review threads unresolved; resolve them. |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Static verification at cd8e2c0 confirms two blocking wallet-replacement defects: incomplete shutdown is ignored, and restoration failure is published as successful empty state. Current CI now reports the Swift refusal regression test failing; Rust workspace, wasm32, and Kotlin checks passed, while book preview failed during the unrelated mdBook download/extraction. The upstream stale-sync tracking request is satisfied by rust-dashcore#1366, and the supplied base changes do not supersede the remaining findings.
🔴 2 blocking
2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The large, intricate change directly changes proof-verification key handling and trust-source selection in packages/rs-context-provider/src/routing.rs and packages/rs-platform-wallet/src/spv_context_provider.rs, determining which quorum public keys are accepted for Platform proofs. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/WalletManagerStore.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/WalletManagerStore.swift:262-266: Do not replace a manager after a non-clean shutdown
(existing thread: https://github.com/dashpay/platform/pull/3417#discussion_r4416654088)
`shutdown()` now returns the native teardown verdict through `PlatformWalletShutdownMetrics.allWorkersStopped`, but activation discards it and immediately restores the successor from the same persistence scope. Rust destroy removes the handle even when it returns `ErrorShutdownIncomplete`; a surviving worker retains its persister and owned callback context and can still write afterward. Those lifetime guarantees prevent use-after-free, not stale overwrites of balances, nonces, or synchronization state written by the replacement. A newly rebuilt SDK has its own source slot, so slot exclusivity does not prevent this overlap.
Require a clean shutdown before restoring or publishing the successor. On refusal, explicitly shut down the unused replacement, expose a restart-required/unavailable state, and retain the refusal for that persistence scope across later activation and background-manager requests. Merely retaining the predecessor's revoked handle is not a usable rollback. Current Swift CI confirms the missing behavior: job https://github.com/dashpay/platform/actions/runs/42572897360/job/129021078416 lists `WalletManagerStoreActivateTests.testShouldRefuseTheReplacementWhileTheReplacedManagerHasLiveWorkers()` under `Failing tests` and ends with `** TEST FAILED **`. This is the same lifecycle defect, not a separate test-only finding.
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/WalletManagerStore.swift:297-307: Do not publish an empty replacement after wallet restoration fails
(existing thread: https://github.com/dashpay/platform/pull/3417#discussion_r4490717728)
`loadFromPersistor()` propagates a failed bulk persistence load, but this helper catches it and returns normally. Activation has already shut down the predecessor before calling the helper, then unconditionally caches and publishes the fresh manager. An unreadable wallet store therefore becomes an apparently healthy empty wallet set, and the rebuild coordinator also publishes the new SDK. A subsequent activation using that SDK takes the cache-hit return without retrying hydration.
Although catch-and-log restoration existed on the base, this PR moves it into this helper and relies on that success-shaped result in its new shutdown-before-restoration replacement lifecycle, so the issue remains in scope. Preserve the restoration failure through activation, explicitly dispose of the unpublished replacement, and expose an unavailable/retryable restoration state rather than enabling normal wallet flows over apparent absence. The Rust loader already distinguishes load failure from an empty store and rolls back partial hydration; the coordinator must not discard that distinction. Add a restoration-failure activation test alongside the configuration-failure coverage.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Expose incomplete teardown to Kotlin lifecycle consumers — JNI logs the native shutdown report but returns Unit, so Kotlin cannot distinguish surviving workers before closing its persistence executor or permitting another manager to open the same state. These lifecycle behaviors predate this PR; the changed logging does not introduce them, but they warrant a dedicated correctness follow-up.
- Follow-up: A follow-up PR should propagate the native destroy verdict through JNI and coordinate Kotlin persistence-resource lifetime and manager replacement with that verdict.
Reverts 593e2df, 2526d89, 1bcb15d, ed0b3fd and cd8e2c0. Replacing a manager whose shutdown left workers running, and a wallet restore that fails during the replacement, both predate this PR. On v5.1-dev the replaced manager was never shut down at all, and a failed restore already published an empty manager. They move to a follow-up so this PR stays on SPV quorum verification. No behaviour change against 1ab26d6: the tree is identical. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Your move: thepastaclaw left review threads unresolved; resolve them. |
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
At e15b2df, the SPV trust prerequisites, fail-closed routing, contract-cache sharing, and prior boundary fixes are present; two non-blocking issues remain in native provider composition and the app's source indicator. Current CI passes Rust workspace, wasm32, Swift SDK, Kotlin, and JavaScript checks; this verification was static-only, with no local builds or tests. The pre-existing manager replacement failures remain tracked in #5816, and the supplied eleven-commit base drift contains no reported merge conflicts or changes superseding this integration.
🟡 2 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The large, cross-cutting change directly alters quorum public-key sourcing for cryptographic proof verification in packages/rs-context-provider/src/routing.rs and packages/rs-platform-wallet/src/spv_context_provider.rs, including trust-source fallback and SPV readiness checks. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-context-provider/src/routing.rs`:
- [SUGGESTION] packages/rs-context-provider/src/routing.rs:311-321: Reject mismatched networks when composing an adaptive provider
The constructor accepts the routing network independently of `trusted.network()`, then `network()` advertises only the routing network. Wrapping a Testnet `VerifiedHttpContextProvider` with `Network::Mainnet` therefore passes the Mainnet SDK builder's network check even though installing that child directly would be rejected. Auto before SPV readiness, and Trusted mode, subsequently use the Testnet provider while reporting Mainnet; proof verification does not universally catch this mismatch afterward, since identity verification ignores its network argument and Tenderdash signature verification uses the response's chain ID. The existing FFI factory supplies matching networks, so this is a native-composition safeguard rather than a demonstrated regression in the shipped Swift path. Reject a child that declares a different network in the routing owner and add a regression using a child that returns `Some(other_network)`; the current requirements test uses a child with no declared network.
In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift:185-187: Refresh the quorum-source indicator when SPV stops running
Readiness depends on running state as well as progress. At the pinned dash-spv revision, a failed continuous-sync loop clears `running`, and coordinator shutdown retains the last progress snapshot. `PlatformWalletManager.applyManagerSnapshot` can consequently publish `spvIsRunning = false` without changing `spvProgress`. Auto then switches native proof verification to trusted HTTP, but this observer does not refresh `AppState.quorumSource`, leaving Options displaying the green “SPV (trustless)” label until another progress or mode change. Observe `spvIsRunning` too and add coverage for a running-to-stopped transition with unchanged progress.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Complete the tracked wallet-manager replacement lifecycle repair — Native destroy still returns success after an incomplete shutdown, and restoration errors still permit publication of an empty replacement. Both deficiencies predate this integration, but can leave overlapping persistence workers or present unavailable wallets as absent.
- Follow-up: Resolve #5816 in a separate lifecycle PR that propagates shutdown completeness, prevents overlapping persistence ownership, and makes restoration failure explicitly unavailable and retryable.
| pub fn new( | ||
| trusted: Arc<dyn ContextProvider>, | ||
| contracts: Arc<dyn ContractSource>, | ||
| network: Network, | ||
| ) -> Self { | ||
| Self { | ||
| trusted, | ||
| spv: Arc::default(), | ||
| contracts, | ||
| mode: AtomicU8::new(ContextProviderMode::Auto as u8), | ||
| network, |
There was a problem hiding this comment.
🟡 Suggestion: Reject mismatched networks when composing an adaptive provider
The constructor accepts the routing network independently of trusted.network(), then network() advertises only the routing network. Wrapping a Testnet VerifiedHttpContextProvider with Network::Mainnet therefore passes the Mainnet SDK builder's network check even though installing that child directly would be rejected. Auto before SPV readiness, and Trusted mode, subsequently use the Testnet provider while reporting Mainnet; proof verification does not universally catch this mismatch afterward, since identity verification ignores its network argument and Tenderdash signature verification uses the response's chain ID. The existing FFI factory supplies matching networks, so this is a native-composition safeguard rather than a demonstrated regression in the shipped Swift path. Reject a child that declares a different network in the routing owner and add a regression using a child that returns Some(other_network); the current requirements test uses a child with no declared network.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)
| .onChange(of: walletManager.spvProgress) { _, _ in | ||
| platformState.applyQuorumMode() | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Refresh the quorum-source indicator when SPV stops running
Readiness depends on running state as well as progress. At the pinned dash-spv revision, a failed continuous-sync loop clears running, and coordinator shutdown retains the last progress snapshot. PlatformWalletManager.applyManagerSnapshot can consequently publish spvIsRunning = false without changing spvProgress. Auto then switches native proof verification to trusted HTTP, but this observer does not refresh AppState.quorumSource, leaving Options displaying the green “SPV (trustless)” label until another progress or mode change. Observe spvIsRunning too and add coverage for a running-to-stopped transition with unchanged progress.
| .onChange(of: walletManager.spvProgress) { _, _ in | |
| platformState.applyQuorumMode() | |
| } | |
| .onChange(of: walletManager.spvProgress) { _, _ in | |
| platformState.applyQuorumMode() | |
| } | |
| .onChange(of: walletManager.spvIsRunning) { _, _ in | |
| platformState.applyQuorumMode() | |
| } |
source: gpt-6.1-sol (phase2-reviewer: general)
|
Bots are done — your move: post |
Brings in 22 commits of v5.1-dev up to 9b36712 (#5821). Conflicts: - rs-sdk-trusted-context-provider/src/lib.rs: keep both modules, `known_contracts` (#5809) and `local` (this PR). - rs-sdk-trusted-context-provider/src/provider.rs and local.rs: #5809 (a8b4b49) gave each provider's contract cache one rule, `hold_unless_newer_held`: adding, hydrating and registering a contract keeps a held contract of a higher version. It also added `replace_known_contract` for an explicit replacement. Here the trusted provider's cache is `LocalContextResolver`, so the rule moves into the resolver, which stays the single owner of the cache: - `add_known_contract`, `add_known_data_contract` (the resolver's `ContractSource::register_data_contract`, which the SPV and adaptive providers use) and `add_known_contracts` go through `hold_unless_newer_held`; - the resolver gains `replace_known_contract`, and the trusted provider's methods delegate to it; - `add_known_contract_if_newer` is unchanged. This replaces this PR's earlier choice that a registration overwrites (0e7dab3). v5.1-dev's `ContextProvider::register_data_contract` now asks every provider to keep a newer held contract, and the moderation flow registers the contract's current version. An equal version now replaces the held body in a bulk preload too, as in both providers' caches; the preload test's equal-version case asserts that. Both sides' tests are kept. #5809's two provider tests import `DataContractV0Getters` locally, since the module no longer does. - rs-sdk/src/sdk/protocol_gate_tests.rs: #5769 (0a38441, via the v5.0-dev merge 356be94) and #5809 added tests. The helpers and the NoQuorumSource / NoContractSource tests are kept. Dropped, because the SDK's context provider is fixed and non-optional here, so the state cannot be built: - `should_refuse_without_asking_when_answers_cannot_be_verified`; - `should_fail_a_proved_fetch_once_and_ban_no_node_without_a_context_provider`; - the "without a context provider" case of `should_not_take_a_version_verified_earlier_as_the_network_below_now`. Adapted outside the conflicts: - wasm-sdk/src/private_contact_requests.rs (#5811): two tests called `Sdk::set_context_provider`, which this PR removes. They pass the context to `SdkBuilder::with_context_provider` instead. - rs-sdk/src/error.rs (#5769): the "context provider is not set" message no longer names `Sdk::set_context_provider`. - Docs: `dash_sdk_add_known_contracts` (an older contract is ignored, no longer "not newer"), `ContractSource::register_data_contract` (keeps a newer held contract, as the trait asks), and the `known_contracts` module. `TrustedHttpContextProvider` still reports no `network()`: #5766 is an open issue, not part of v5.1-dev. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ve provider `AdaptiveContextProvider::new` took its routing network separately from its trusted provider's, and `network()` reported only the routing one. `SdkBuilder::build` checks the network of the provider it is given, so a Testnet `VerifiedHttpContextProvider` wrapped as Mainnet passed a Mainnet builder, although the builder refuses that child installed on its own. Auto before SPV is ready, and Trusted mode, then verified proofs with Testnet quorum keys while the provider reported Mainnet. `new` now returns `Result` and fails with `ContextProviderError::Config` when the trusted provider declares a different network. A trusted provider that declares none (`TrustedHttpContextProvider` today) is accepted, as the builder accepts it. `fixed_provider_parts` in rs-sdk-ffi maps the failure to `InternalError`, like the trusted provider's own construction failures; the FFI passes one network to both, so it cannot happen there. `SpvContextProvider` takes no child with a network: its `ContractSource` has none, and the SPV source the wallet publishes into its slot later is built from the SDK's own network. Addresses thepastaclaw's review thread on routing.rs:321. Test would have caught this in CI: should_refuse_a_trusted_provider_for_another_network ✖ with `new` fallible but without the check: the Testnet child under a Mainnet adaptive provider was accepted (panicked: `None`) ✔ after the check (cargo test -p dash-context-provider --all-features --lib: 18 passed) Co-Authored-By: Claude Opus 5.5 <[email protected]>
…stops running The adaptive provider's readiness needs SPV running as well as synced. At the pinned dash-spv revision a failed continuous-sync loop clears `running` and coordinator shutdown keeps the last progress snapshot, so `PlatformWalletManager` can publish `spvIsRunning = false` with `spvProgress` unchanged. Auto then routes proof verification to the trusted service, but only progress changes refreshed `AppState.quorumSource`, so Options kept showing "SPV (trustless)". The `spvProgress` observer also could not fire on its own: it sat on the App scene's content, and the scene observes `WalletManagerStore`, which publishes only when the active manager is swapped, not the manager. An `.onChange` there re-read the manager's values only when another of the scene's objects happened to publish. Both observers now live in `QuorumSourceRefresh`, a leaf behind the content that holds the manager as `@ObservedObject`, the pattern `GlobalSyncIndicatorOverlay` follows; it calls `applyQuorumMode` when `spvProgress` or `spvIsRunning` changes. Once the observer fires on every progress tick, `applyQuorumMode` assigning an unchanged `quorumSource` would publish `AppState` each tick and re-render every view observing it, `ContentView`'s tab view among them. It now assigns only a changed source. Its doc names the running state. No test: the observers are view modifiers, and SwiftExampleAppTests has no way to drive a SwiftUI view (no ViewInspector or similar). Pulling the observation into a testable object only for this would be a larger change than the fix. pasta has no iOS SDK, so the files were checked only with `swiftc -parse`; the Swift SDK CI job builds them. Addresses thepastaclaw's review thread on SwiftExampleAppApp.swift:187. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… registration The v5.1-dev merge moved #5809's keep-newer rule into `LocalContextResolver`, so `ContractSource::register_data_contract`, the only way the SPV and adaptive providers register a contract, went from overwriting to keeping a held contract of a higher version. Only the trusted provider's tests checked the rule; the resolver's own test registered into an empty cache. The new test registers versions 2, 3 and then 2 through the `ContractSource` path, expects 3 to stay, and expects `replace_known_contract` to replace it. Test would catch a regression: should_keep_a_newer_contract_over_a_registered_older_one ✖ with `add_known_data_contract` overwriting again, as before the merge: "an older registration does not" (left: Some(2), right: Some(3)) ✔ as merged (cargo test -p rs-sdk-trusted-context-provider --all-features --lib: 155 passed; default features: 142 passed) Co-Authored-By: Claude Opus 5.5 <[email protected]>
Issue being fixed or feature implemented
The iOS app can verify Platform proofs with quorum public keys from its own SPV client instead of relying only on the centralized quorum HTTP service. SDK consumers choose the trust model when they build the SDK: Trusted, strict SPV, or Adaptive (switchable Auto/SPV/Trusted, used by SwiftExampleApp).
Trust root. SPV quorum keys are as trustworthy as dash-spv's checks at Platform's rust-dashcore pin. That pin is
c9aeac0d, from v5.1-dev via #5729:Verified.ChainLock anchoring is not part of this trust root. dashpay/rust-dashcore#1309 was closed in favour of DIP 175 quorum proof chains (dashpay/rust-dashcore#1309 (comment)), which are a follow-up (see Follow-ups).
What was done?
Providers (
dash-context-provider::routing):SpvContextProvidertakes quorum keys only from an SPV client and never from the trusted service.AdaptiveContextProviderroutes each lookup by mode:core_height_verified, because it does not record which source supplied a quorum's key.ContractSource, which has no quorum keys.SdkBuilder::build. The adaptive provider also reports its trusted provider's proof requirement, so the builder's provider checks apply to them.AdaptiveContextProvider::newrefuses a trusted provider that declares another network, which the builder cannot see behind it.SpvSourceSlotis where an SPV client publishes its quorum lookups. A client claims it and holds it through a lease, one lease at a time.QuorumSourceUnavailable, so the SDK does not ban the node whose proof it could not check.Local contract cache (
rs-sdk-trusted-context-provider):TrustedHttpContextProvideris nowLocalContextResolver. The trusted provider delegates to it.system_contractsloader, which includes the decision-proposals and token-market contracts from protocol 15.replace_known_contractreplaces on purpose.FFI (
rs-sdk-ffi):dash_sdk_create_trusted,dash_sdk_create_spvanddash_sdk_create_adaptivebuild the matching provider. Strict SPV never builds the trusted HTTP provider and ignoresquorum_url.dash_sdk_add_known_contractsuse the SDK's local contract cache, so they work for every kind. Trusted and Adaptive share the trusted provider's cache with proof verification.dash_sdk_set_context_provider_modeanddash_sdk_get_context_provider_sourcedrive the adaptive provider.Wallet (
rs-platform-wallet,rs-platform-wallet-ffi):platform_wallet_manager_create_with_sdk(with the persistence and event extensions) claims the SDK's slot before it builds the manager. A second manager on the same SDK is refused before it takes the host's callback contexts.The manager serves its SPV quorum keys from creation until shutdown or drop.
The source answers only while all of these hold:
Synced;dash-spv keeps masternode sync
Syncedwhen later list updates fail. Without the tip check, a stale list would count as ready and its misses would get honest nodes banned.Only quorums dash-spv marked
Verifiedare served. Quorum types that name no LLMQ type are refused.Swift:
SDK(network:contextProvider:)defaults to Trusted.SDK.quorumSourcereads the native provider.SDK.QuorumModedocuments each mode.PlatformWalletManager.configure(sdk:)creates the manager throughcreate_with_sdk.Merged with v5.1-dev at
9b36712770(earlier at46c26cb05f). The first merge brought in the rust-dashcore pin above, the verified quorum provider (#5523, #5720) and its SDK opt-in (#5721). The second:LocalContextResolver. Registration no longer overwrites a newer held contract, as this PR first had it;set_context_provider.Earlier merge details:
Sdk::set_context_providerchecks now run inSdkBuilder::build, since the provider is fixed at build. Its tests now test the builder.How Has This Been Tested?
47e3355613, after the second v5.1-dev merge:dash-context-provider, its env,--all-features --locked --lib --bins --tests -- --skip shield): 21,980 passed, 0 failed, 70 ignored, across 104 test binaries. That run predates only the last commit, which adds one test.rs-sdk-trusted-context-provider: 155 passed (all features), 142 (default features).wasm-sdk: 221 native tests passed.cargo clippy --all-targets --all-features -- -D warningsclean ondash-context-provider,rs-sdk-trusted-context-provider,dash-sdk,rs-sdk-ffi,platform-wallet,platform-wallet-ffi,wasm-sdk; the context-provider crates also without features.should_refuse_a_trusted_provider_for_another_network(red with the constructor fallible but unchecked) and the resolver'sshould_keep_a_newer_contract_over_a_registered_older_one(red with registration overwriting, as before the merge).swiftc -parsehere; the Swift SDK job builds it. The indicator fix has no test, as SwiftExampleAppTests cannot drive a view's modifiers.Earlier rounds:
cargo test --all-features --locked --lib --bins --tests -- --skip shield, CI's package list and env):1655562fbf.strategy_testsbinary ran, not only the two tests PR CI picks.platform-wallet,dash-context-provider,rs-sdk-trusted-context-provider(all features): 1,770 + 17 + 150 unit tests passed. The trusted provider's default-feature build: 138 passed.platform-wallet-storageshield-named SQLite tests: 7 passed.wasm-sdk: 217 native tests passed, and 45 wasm32 tests passed underwasm-bindgen-test-runner.cargo clippy --all-targets --all-features -- -D warnings, with no warnings, on:dash-context-provider,rs-sdk-trusted-context-provider,dash-sdk,platform-wallet,rs-sdk-ffi,platform-wallet-ffi,dash-platform-balance-checker;wasm-sdkfor wasm32.cargo check --workspace --all-targets.should_not_be_ready_while_the_masternode_list_trails_the_header_tip;should_forward_provider_requirements_through_every_wrapper;SDK.quorumSourcefor a strict SPV SDK (Swift).swiftc -parse). The Swift SDK job is the build and test gate.Known limits:
Verifiedis reported as unavailable. A node can name one without being banned.Follow-ups:
dash_sdk_add_known_contractsaccepts system-contract IDs.VerifiedHttpContextProviderbefore the trusted service, on networks whose relay serves/proofs. Today that is testnet only: mainnet's relay refuses/proofs, and Core v24 is not released.core_height_verifiedto the source that supplied a key comes with persisting the verified provider'scontinuation().VerifiedHttpContextProviderkeeps its own contract and token caches and lookup, which duplicateLocalContextResolver. Delegate to the resolver.server, and belongs indpp.TrustedHttpContextProviderreports nonetwork()(feat(sdk): make TrustedHttpContextProvider report its network so the SDK refuses a mismatch #5766);Invalidreads as missing, so its lookups are charged to the node.Syncedwhile its newest list lags the header tip after given-up tip updates; this PR's readiness adds a two-block lag check. Tracked upstream in dash-spv: masternode sync staysSyncedwhile its newest list falls behind the header tip rust-dashcore#1366.Breaking Changes
None that affect consensus. SDK API changes:
Sdk::set_context_provider, which v5.1-dev made fallible in feat(sdk)!: let wasm-sdk and evo-sdk opt into the verified quorum context provider #5721. The context provider is fixed at SDK construction, and its network and proof requirement are checked bySdkBuilder::build.Sdk::context_provider()now returnsOption<Arc<dyn ContextProvider>>instead ofOption<impl ContextProvider>.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull request🤖 Generated with Claude Code
PR Hygiene ·
e15b2df/self-reviewedrs-platform-wallet-ffi(packages/rs-platform-wallet-ffi/Cargo.toml,packages/rs-platform-wallet-ffi/src/manager.rs) — HashEngineering or ZocoLini or llbartekll or romchornyirs-platform-wallet(packages/rs-platform-wallet/Cargo.toml,packages/rs-platform-wallet/src/lib.rs,packages/rs-platform-wallet/src/manager/mod.rsand 3 more) — HashEngineering or ZocoLini or llbartekll or romchornyirust-sdk-ffi(packages/rs-sdk-ffi/Cargo.toml,packages/rs-sdk-ffi/src/document/create.rs,packages/rs-sdk-ffi/src/document/delete.rsand 11 more) — lklimek or shumkovrust-sdk(packages/rs-sdk/examples/read_contract.rs,packages/rs-sdk/src/sdk.rs,packages/rs-sdk/src/sdk/protocol_gate_tests.rsand 1 more) — lklimek or shumkovswift-sdk(packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift,packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift,packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swiftand 11 more) — llbartekll or romchornyiWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.