Skip to content

feat(rs-platform-wallet-ffi): expose devnet name and LLMQ_DEVNET override in spv_start#3758

Merged
QuantumExplorer merged 1 commit into
v3.1-devfrom
claude/wizardly-feistel-bee0c8
May 27, 2026
Merged

feat(rs-platform-wallet-ffi): expose devnet name and LLMQ_DEVNET override in spv_start#3758
QuantumExplorer merged 1 commit into
v3.1-devfrom
claude/wizardly-feistel-bee0c8

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 27, 2026

Issue being fixed or feature implemented

SPV-syncing a Dash devnet from the platform wallet currently doesn't work — the FFI's platform_wallet_manager_spv_start has no way to pass the two devnet-specific knobs that rust-dashcore PR dashpay/rust-dashcore#784 recently exposed on ClientConfig (already pinned at 58d61ea in our Cargo.toml):

  1. Devnet user-agent substring. Dash Core peers running a devnet gate inbound handshakes on a devnet.devnet-<name> substring in the peer's user agent (net_processing.cpp:3957). Without it, the SPV client cannot connect to any devnet peer.
  2. LLMQ_DEVNET size/threshold override. Mirrors Dash Core's -llmqdevnetparams=<size>:<threshold>. Without matching params, the SPV client reconstructs the wrong quorum and ChainLock / legacy InstantSend signatures fail to verify.

This PR wires both through the FFI and the Swift wrapper as pure plumbing.

What was done?

Rust FFI (rs-platform-wallet-ffi)platform_wallet_manager_spv_start gains three trailing params:

  • devnet_name: *const c_charNULL means "not devnet".
  • llmq_devnet_size: u32, llmq_devnet_threshold: u32 — both 0 means "no override".

Validations (returned as ErrorInvalidParameter):

  • devnet_name must be set iff network == Devnet.
  • llmq_devnet_size and llmq_devnet_threshold must both be set or both zero.
  • Non-zero LLMQ params are rejected on non-devnet networks (matches ClientConfig::validate() wording for consistency).

When devnet_name is supplied, the user agent is rebuilt as /{base}(devnet.devnet-{name})/, falling back to a platform-wallet-ffi:<crate version> base when the caller didn't pass one — mirrors the format the dash-spv binary constructs in main.rs. The LLMQ override is written into ClientConfig.llmq_devnet_params; DashSpvClient::new applies it via apply_global_overrides (idempotent for identical values).

Swift wrapper (SwiftDashSDK)PlatformSpvStartConfig gains devnetName: String?, llmqDevnetSize: UInt32, llmqDevnetThreshold: UInt32, all defaulted so existing call sites (e.g. CoreContentView.swift) compile unchanged. startSpv(config:) marshals devnetName to a C string with the same strdup/defer free pattern as userAgent, then passes the new trailing args to the FFI. Pure marshalling — no decisions on the Swift side (complies with packages/swift-sdk/CLAUDE.md).

Diff stat: 2 files changed, +93 / -3.

How Has This Been Tested?

  • cargo check -p platform-wallet-ffi — clean.
  • cargo fmt --all -- --check — clean.
  • cd packages/swift-sdk && ./build_ios.sh --target sim — succeeded end-to-end (regenerated the C header inside DashSDKFFI.xcframework, then ran xcodebuild build on SwiftExampleApp with -warnings-as-errors; reports Swift/Xcode build succeeded).

The regenerated header inside the xcframework carries the new signature:

struct PlatformWalletFFIResult platform_wallet_manager_spv_start(Handle handle, const char *data_dir, FFINetwork network, const char *user_agent, const char *const *peers, uintptr_t peer_count, bool restrict_to_configured_peers, uint32_t start_from_height, const char *devnet_name, uint32_t llmq_devnet_size, uint32_t llmq_devnet_threshold);

Breaking Changes

None at the Swift level — every new field on PlatformSpvStartConfig is defaulted (devnetName: nil, llmqDevnetSize: 0, llmqDevnetThreshold: 0), so existing call sites compile unchanged.

The Rust extern "C" signature does gain three trailing params, which is a C-ABI source-level break for any out-of-tree caller calling platform_wallet_manager_spv_start directly. Per feedback_breaking_change_scope, feat!: is reserved for consensus-breaking changes in this repo, so this lands under plain feat:.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

Summary by CodeRabbit

Release Notes

  • New Features
    • SPV wallet manager now supports devnet network configuration with custom devnet naming capabilities
    • Added customizable LLMQ (Quorum) parameters for devnet environments, including size and threshold override options

Review Change Stack

…ride in spv_start

Devnet SPV sync needs two knobs the FFI didn't expose:

- Dash Core devnet peers gate the inbound handshake on a
  `devnet.devnet-<name>` substring in the user agent
  (`net_processing.cpp:3957`). Without it, the SPV client cannot
  connect to any devnet peer.
- `LLMQ_DEVNET` size/threshold must match the devnet's
  `-llmqdevnetparams=<size>:<threshold>` so the SPV client reconstructs
  the right signing sets for ChainLock / legacy InstantSend
  verification.

Both are already library fields on `dash-spv`'s `ClientConfig` after
rust-dashcore PR dashpay/rust-dashcore#784. This change wires them
through the FFI and the Swift wrapper as pure plumbing — no decisions
on either side.

What changed:
- `platform_wallet_manager_spv_start` gains three trailing params:
  `devnet_name: *const c_char`, `llmq_devnet_size: u32`,
  `llmq_devnet_threshold: u32`. `0`/`NULL` mean "leave default".
  Validates `(devnet_name set) <=> (network == Devnet)` and
  `(size > 0) <=> (threshold > 0)` and rejects size/threshold on
  non-devnet networks. When `devnet_name` is set, rebuilds the user
  agent as `/{base}(devnet.devnet-{name})/`, falling back to a
  `platform-wallet-ffi:<version>` base when the caller didn't supply
  one — mirrors the format the `dash-spv` binary uses.
- `PlatformSpvStartConfig` gains `devnetName: String?`,
  `llmqDevnetSize: UInt32`, `llmqDevnetThreshold: UInt32` (all
  defaulted, so existing call sites compile unchanged).
  `startSpv(config:)` marshals them to the new trailing FFI params.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer requested a review from shumkov as a code owner May 27, 2026 19:22
@github-actions github-actions Bot added this to the v3.1.0 milestone May 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32760ce8-2620-4d0e-9de3-6df8ce705dd4

📥 Commits

Reviewing files that changed from the base of the PR and between 3157486 and f59f1ac.

📒 Files selected for processing (2)
  • packages/rs-platform-wallet-ffi/src/spv.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerSPV.swift

📝 Walkthrough

Walkthrough

The pull request adds SPV devnet-specific configuration support to both the Rust FFI module and Swift SDK wrapper. The Rust layer validates devnet parameters, rewrites user-agent strings for devnet identity, and populates LLMQ configuration. The Swift layer exposes these parameters through PlatformSpvStartConfig and forwards them to the FFI boundary.

Changes

SPV Devnet and LLMQ Parameter Support

Layer / File(s) Summary
FFI Devnet Support and Validation
packages/rs-platform-wallet-ffi/src/spv.rs
Imports LlmqDevnetParams, extends platform_wallet_manager_spv_start with devnet configuration parameters, validates that devnet_name is set only on Devnet networks, enforces the constraint that llmq_devnet_size and llmq_devnet_threshold must both be zero or both non-zero, rewrites user_agent to embed /devnet.devnet-<name>/ when devnet is specified, and populates config.llmq_devnet_params when LLMQ overrides are provided.
Swift SDK Devnet Config and FFI Binding
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerSPV.swift
Adds devnetName, llmqDevnetSize, and llmqDevnetThreshold fields to PlatformSpvStartConfig, extends the public initializer to accept these parameters with defaults, allocates and manages a C string for devnetName via defer, and extends the platform_wallet_manager_spv_start FFI call to forward the new parameters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

ready for final review

Suggested reviewers

  • shumkov

Poem

🐰 A devnet dream, so shiny and new,
Parameters flow from Swift straight through,
LLMQ configs dance in the fray,
User agents rewrite to have their say—
Validation guards every hoppy way!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: exposing devnet name and LLMQ_DEVNET override parameters in the spv_start FFI function, which is reflected across both the Rust FFI module and Swift wrapper.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wizardly-feistel-bee0c8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

@QuantumExplorer QuantumExplorer merged commit 2842cbb into v3.1-dev May 27, 2026
17 of 20 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/wizardly-feistel-bee0c8 branch May 27, 2026 19:31
@github-actions
Copy link
Copy Markdown
Contributor

✅ 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: "a8f988ec674ed853368bf44dff653391ae2dc2183128ae12e76778cef6a709fb"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.17%. Comparing base (7b824c2) to head (f59f1ac).
⚠️ Report is 2 commits behind head on v3.1-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3758      +/-   ##
============================================
- Coverage     87.32%   87.17%   -0.16%     
============================================
  Files          2590     2607      +17     
  Lines        316977   319484    +2507     
============================================
+ Hits         276809   278501    +1692     
- Misses        40168    40983     +815     
Components Coverage Δ
dpp 87.71% <ø> (+<0.01%) ⬆️
drive 85.95% <ø> (ø)
drive-abci 89.60% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.14% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Pure FFI plumbing for two devnet knobs (devnet_name and LLMQ_DEVNET size/threshold). The validation matrix is sensible and mirrors the documented contract. Three small input-edge-cases are worth tightening at the boundary; nothing blocks merge. The codex 'blocking' truncation claim could not be verified against the pinned dash-spv revision (58d61ea) and is dropped.

🟡 1 suggestion(s) | 💬 2 nitpick(s)

🤖 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-platform-wallet-ffi/src/spv.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/spv.rs:237-248: Empty `devnet_name` passes the required-on-devnet check
  The check on line 243 only rejects `NULL`. A caller passing an empty (but non-null) C string for `devnet_name` while `network == Devnet` satisfies validation, and the user agent is then rebuilt as `/{base}(devnet.devnet-)/`. That trailing `devnet.devnet-` substring will not match any real devnet's gating prefix, so the SPV client returns `ok()` but silently fails the handshake — precisely the failure mode this PR exists to prevent. Because the Swift wrapper takes `devnetName: String?` (not a validated enum), the FFI is the right place to enforce non-empty.

Comment on lines +237 to +248
let devnet_name_str = if devnet_name.is_null() {
None
} else {
Some(unwrap_result_or_return!(CStr::from_ptr(devnet_name).to_str()).to_string())
};

if net == crate::types::Network::Devnet && devnet_name_str.is_none() {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"devnet_name is required when network=Devnet",
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Empty devnet_name passes the required-on-devnet check

The check on line 243 only rejects NULL. A caller passing an empty (but non-null) C string for devnet_name while network == Devnet satisfies validation, and the user agent is then rebuilt as /{base}(devnet.devnet-)/. That trailing devnet.devnet- substring will not match any real devnet's gating prefix, so the SPV client returns ok() but silently fails the handshake — precisely the failure mode this PR exists to prevent. Because the Swift wrapper takes devnetName: String? (not a validated enum), the FFI is the right place to enforce non-empty.

Suggested change
let devnet_name_str = if devnet_name.is_null() {
None
} else {
Some(unwrap_result_or_return!(CStr::from_ptr(devnet_name).to_str()).to_string())
};
if net == crate::types::Network::Devnet && devnet_name_str.is_none() {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"devnet_name is required when network=Devnet",
);
}
let devnet_name_str = if devnet_name.is_null() {
None
} else {
Some(unwrap_result_or_return!(CStr::from_ptr(devnet_name).to_str()).to_string())
};
if net == crate::types::Network::Devnet
&& devnet_name_str.as_deref().map_or(true, str::is_empty)
{
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"devnet_name is required (non-empty) when network=Devnet",
);
}
if net != crate::types::Network::Devnet && devnet_name_str.is_some() {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"devnet_name is only valid on devnet",
);
}

source: ['claude', 'codex']

Comment on lines +273 to +278
if let Some(name) = devnet_name_str.as_deref() {
let base = user_agent_str
.clone()
.unwrap_or_else(|| format!("platform-wallet-ffi:{}", env!("CARGO_PKG_VERSION")));
user_agent_str = Some(format!("/{base}(devnet.devnet-{name})/"));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: User-agent rebuild double-wraps an already-BIP14-formatted base

If the caller supplies a base already in BIP14 form (e.g. "/myapp:1.0/"), the rewrite emits "//myapp:1.0/(devnet.devnet-name)/" — extra slashes that deviate from the /rust-dash-spv:VERSION(devnet.devnet-NAME)/ format the comment claims to mirror. Dash Core's substring filter still matches, so this is cosmetic, but normalizing the base avoids surprising peer-log fingerprints.

Suggested change
if let Some(name) = devnet_name_str.as_deref() {
let base = user_agent_str
.clone()
.unwrap_or_else(|| format!("platform-wallet-ffi:{}", env!("CARGO_PKG_VERSION")));
user_agent_str = Some(format!("/{base}(devnet.devnet-{name})/"));
}
if let Some(name) = devnet_name_str.as_deref() {
let base = user_agent_str
.clone()
.unwrap_or_else(|| format!("platform-wallet-ffi:{}", env!("CARGO_PKG_VERSION")));
let base = base.trim_matches('/');
user_agent_str = Some(format!("/{base}(devnet.devnet-{name})/"));
}

source: ['claude']

Comment on lines +255 to +266
if (llmq_devnet_size > 0) ^ (llmq_devnet_threshold > 0) {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_size and llmq_devnet_threshold must both be set or both zero",
);
}
if llmq_devnet_size > 0 && net != crate::types::Network::Devnet {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_params is only valid on devnet",
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: No threshold <= size check at the FFI boundary

The XOR check ensures both LLMQ params are zero or both nonzero, but accepts size=3, threshold=10. Downstream ClientConfig::validate() is expected to catch it, but enforcing at the FFI surface produces a direct ErrorInvalidParameter instead of a less direct downstream failure. Low priority — confirm whether spawn_in_background actually invokes validate() before applying the config; if it does, this is purely cosmetic.

Suggested change
if (llmq_devnet_size > 0) ^ (llmq_devnet_threshold > 0) {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_size and llmq_devnet_threshold must both be set or both zero",
);
}
if llmq_devnet_size > 0 && net != crate::types::Network::Devnet {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_params is only valid on devnet",
);
}
if (llmq_devnet_size > 0) ^ (llmq_devnet_threshold > 0) {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_size and llmq_devnet_threshold must both be set or both zero",
);
}
if llmq_devnet_threshold > llmq_devnet_size {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_threshold must be <= llmq_devnet_size",
);
}
if llmq_devnet_size > 0 && net != crate::types::Network::Devnet {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"llmq_devnet_params is only valid on devnet",
);
}

source: ['claude']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants