Skip to content

feat: devnet support for dash-spv#784

Merged
QuantumExplorer merged 8 commits into
devfrom
feat/devnet-support
May 27, 2026
Merged

feat: devnet support for dash-spv#784
QuantumExplorer merged 8 commits into
devfrom
feat/devnet-support

Conversation

@xdustinface
Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface commented May 27, 2026

  • Expose devnet in --network.
  • Return the genesis hash for Network::Devnet so storage can initialize.
  • Add --devnet-name, embedded in the user agent as devnet.devnet-<name> to pass Dash Core's devnet handshake check.
  • Route Network::Devnet platform quorum to LlmqtypeDevnetPlatform, matching Dash Core's default.
  • Add ClientConfig::llmq_devnet_params (CLI --llmq-devnet-params=<size>:<threshold>) to mirror Dash Core's -llmqdevnetparams override.

Summary by CodeRabbit

  • New Features

    • Added native devnet support via --network=devnet with required --devnet-name.
    • New option to override devnet quorum parameters (--llmq-devnet-params=SIZE:THRESHOLD); overrides are applied at startup and take effect process-wide.
    • Devnet-specific user agent includes the provided devnet name.
  • Bug Fixes / Validation

    • Supplying devnet-only options on non-devnet networks now errors.
    • Devnet now returns a known genesis block instead of none.

Review Change Stack

The static height-0 devnet genesis block has a deterministic hash (verified by the existing `devnet_genesis_full_block` test). Returning `None` here caused `dash-spv` to fail with `"No known genesis hash for network"` when initializing storage for a devnet. Only the height-1 "devnet genesis" varies per devnet name, height 0 does not.
…m quorum

`platform_type()` returned `LlmqtypeDevnet`, but Dash Core's devnet defaults use `LLMQ_DEVNET_PLATFORM` for `llmqTypePlatform` (see `chainparams.cpp:651`). This matches the standard devnet config `llmqplatform=llmq_devnet_platform`.
Allows targeting `Network::Devnet` from the `dash-spv` binary. Required for SPV-syncing a devnet, since previously only mainnet, testnet, and regtest were exposed.
Dash Core nodes on devnet check that incoming peers carry `devnet.<network-id>` in their user agent and disconnect otherwise (see `net_processing.cpp:3957-3967`). The `network-id` is `devnet-<name>` per `ArgsManager::GetDevNetName`, so the substring required for the `paloma` devnet is `devnet.devnet-paloma`.

The new flag is required whenever `--network=devnet`, and the user agent is rebuilt as `/rust-dash-spv:<version>(devnet.devnet-<name>)/` so devnet peers accept the handshake.
Dash Core's `-llmqdevnetparams=<size>:<threshold>` lets a devnet adjust the `LLMQ_DEVNET` quorum size and threshold (`chainparams.cpp:UpdateLLMQDevnetParameters`). Without matching params, the SPV client picks the wrong masternodes when reconstructing the signing set, so ChainLock and legacy InstantSend signatures cannot be verified on devnets that use the override.

`ClientConfig::llmq_devnet_params: Option<(u32, u32)>` carries the override and `DashSpvClient::new` applies it after `validate`. Internally this writes to a `OnceLock` in `dashcore::sml::llmq_type` that `LLMQType::params()` consults for `LlmqtypeDevnet`, mirroring Dash Core's chainparams-as-global model without exposing the setter beyond the lib. Only `LLMQ_DEVNET` is affected (matches Dash Core, the DIP0024 and platform devnet quorums are not adjusted by this flag).

The `dash-spv` binary exposes the override as `--llmq-devnet-params=<size>:<threshold>`, valid only with `--network=devnet`.
@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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f0494298-a1d8-474a-b11d-0b086bef0cb4

📥 Commits

Reviewing files that changed from the base of the PR and between e3c8002 and a5b5204.

📒 Files selected for processing (3)
  • dash-spv/src/client/config.rs
  • dash-spv/src/main.rs
  • dash/src/sml/llmq_type/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • dash-spv/src/main.rs

📝 Walkthrough

Walkthrough

Adds devnet LLMQ runtime overrides, CLI flags and client config support for devnet parameters, a hardcoded devnet genesis hash, and applies configured overrides process-wide during client startup.

Changes

Devnet LLMQ Configuration and Runtime Overrides

Layer / File(s) Summary
Runtime Override Mechanism
dash/src/sml/llmq_type/mod.rs
Core OnceLock-backed infrastructure enables one-time process-global LLMQ devnet parameter configuration via set_llmq_devnet_params(), effective values returned by llmq_devnet_params(), and LLMQType::params() updated to use the runtime overrides.
Network Devnet Support
dash/src/network/constants.rs, dash/src/sml/llmq_type/network.rs
Hardcoded genesis block hash for Network::Devnet and platform LLMQ type mapping updated to LlmqtypeDevnetPlatform for devnet support.
Client Configuration
dash-spv/src/client/config.rs
ClientConfig extended with optional llmq_devnet_params field, builder method with_llmq_devnet_params(), network validation rejecting devnet params on non-devnet networks, and apply_global_overrides() applies settings at startup.
CLI and Lifecycle Integration
dash-spv/src/main.rs, dash-spv/src/client/lifecycle.rs
CLI adds --network=devnet, --devnet-name, and --llmq-devnet-params SIZE:THRESHOLD arguments; configuration parses and applies devnet-specific user agent and LLMQ overrides; DashSpvClient::new calls apply_global_overrides() after validation.

Sequence Diagram(s)

sequenceDiagram
  participant User as User / CLI
  participant Args as Args parsing
  participant ClientConfig as ClientConfig
  participant LLMQ as sml::llmq_type
  participant OnceLock as OnceLock store
  User->>Args: supply --network=devnet + --devnet-name + --llmq-devnet-params
  Args->>ClientConfig: build config with llmq_devnet_params
  ClientConfig->>LLMQ: apply_global_overrides() calls set_llmq_devnet_params(size,threshold)
  LLMQ->>OnceLock: set override once
  LLMQ->>LLMQ: llmq_devnet_params() computes effective LLMQParams
  ClientConfig->>DashSpvClient: proceed with validated & applied overrides
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

ready-for-review

Poem

"I thumped a flag and parsed the rest,
Devnet names and sizes pressed,
OnceLock hugs the quorum tight,
Overrides set, everything's right,
Rabbit hops—LLMQ's dressed." 🐇✨

🚥 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 pull request title 'feat: devnet support for dash-spv' directly and clearly summarizes the main change: adding devnet support to the dash-spv client and library.
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 feat/devnet-support

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

❌ Patch coverage is 55.44554% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.26%. Comparing base (7abc02e) to head (a5b5204).

Files with missing lines Patch % Lines
dash-spv/src/main.rs 0.00% 31 Missing ⚠️
dash-spv/src/client/config.rs 57.14% 6 Missing ⚠️
dash/src/network/constants.rs 0.00% 5 Missing ⚠️
dash/src/sml/llmq_type/mod.rs 95.91% 2 Missing ⚠️
dash/src/sml/llmq_type/network.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #784      +/-   ##
==========================================
+ Coverage   72.24%   72.26%   +0.01%     
==========================================
  Files         321      321              
  Lines       70370    70467      +97     
==========================================
+ Hits        50839    50922      +83     
- Misses      19531    19545      +14     
Flag Coverage Δ
core 76.33% <85.45%> (+0.03%) ⬆️
ffi 46.21% <ø> (+0.29%) ⬆️
rpc 20.00% <ø> (ø)
spv 89.68% <19.56%> (-0.21%) ⬇️
wallet 71.21% <ø> (ø)
Files with missing lines Coverage Δ
dash-spv/src/client/lifecycle.rs 78.12% <100.00%> (+0.11%) ⬆️
dash/src/sml/llmq_type/network.rs 73.19% <0.00%> (ø)
dash/src/sml/llmq_type/mod.rs 71.64% <95.91%> (+6.22%) ⬆️
dash/src/network/constants.rs 84.07% <0.00%> (-2.20%) ⬇️
dash-spv/src/client/config.rs 88.46% <57.14%> (-4.88%) ⬇️
dash-spv/src/main.rs 0.00% <0.00%> (ø)

... and 18 files with indirect coverage changes

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dash-spv/src/client/config.rs`:
- Around line 226-233: The comment and implementation disagree because
set_llmq_devnet_params currently uses a OnceLock that errors on any second set;
change apply_global_overrides (and/or set_llmq_devnet_params) so repeated calls
with the same llmq_devnet_params (size, threshold) are treated as idempotent:
when self.llmq_devnet_params is Some((size, threshold)) first check the existing
stored params via the OnceLock/get API (or equivalent accessor) and if a value
is already set compare it to (size, threshold) and return Ok(()) when they
match, but return Err(...) only when they conflict; only call OnceLock::set (or
perform initialization) when no value is present. Ensure you reference
apply_global_overrides, llmq_devnet_params, set_llmq_devnet_params and the
OnceLock-backed storage when making the change so multiple clients can apply
identical overrides without error.

In `@dash-spv/src/main.rs`:
- Around line 226-231: Validate and sanitize args.devnet_name before embedding
it into user_agent: ensure devnet_name (the value read from args.devnet_name and
used when building user_agent and passed to config.with_user_agent) contains
only allowed characters (e.g. restrict to a safe regex like alphanumeric plus -
and _ or at minimum reject control characters, whitespace and reserved
characters such as parentheses and slashes) and return an error (ok_or) when it
fails validation; update the code path that computes user_agent to perform this
validation/sanitization on devnet_name and only format the user_agent after the
value is confirmed safe.

In `@dash/src/network/constants.rs`:
- Around line 75-81: The Devnet genesis hash in the Network::Devnet match arm is
currently identical to Network::Regtest and will incorrectly gate validation;
update the Network::Devnet arm in constants.rs (the match handling
Network::Devnet / BlockHash construction) to use the correct devnet genesis hash
bytes instead of the regtest literal (or obtain it from the authoritative devnet
constant/source), keep the same reverse() + BlockHash::from_byte_array flow, and
ensure the provided hex decodes to 32 bytes so it no longer matches the Regtest
value.

In `@dash/src/sml/llmq_type/mod.rs`:
- Around line 211-230: Add three tests that exercise the process-wide override:
write test_valid_override to call set_llmq_devnet_params(size,threshold) with a
valid pair and then assert llmq_devnet_params() reflects size, min_size,
threshold and dkg_params.bad_votes_threshold; write
test_invalid_override_rejected to call set_llmq_devnet_params with invalid
values (e.g., threshold > size or zero) and assert it returns Err; write
test_second_set_fails to call set_llmq_devnet_params successfully then call it
again and assert the second call returns Err; because LLMQ_DEVNET_OVERRIDE is
process-global, run each test in its own process (spawn the test binary with
--exact <test_name> or use Command to re-exec the current test binary) so the
OnceLock is fresh for each case; reference functions/setter llmq_devnet_params,
set_llmq_devnet_params and static LLMQ_DEVNET_OVERRIDE and type LLMQParams when
locating code to assert fields.
- Around line 216-227: The override setter set_llmq_devnet_params currently
allows invalid tuples (size==0, threshold==0, threshold>size) which break quorum
logic; add input validation in set_llmq_devnet_params to reject size == 0 or
threshold == 0 and to reject threshold > size (return Err with a clear static
message), only call LLMQ_DEVNET_OVERRIDE.set when inputs pass validation; keep
llmq_devnet_params and LLMQ_DEVNET usage unchanged but rely on the validated
override to ensure size/min_size/threshold remain consistent.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a24a4f0-0204-4d03-a733-c3f324b3e9c0

📥 Commits

Reviewing files that changed from the base of the PR and between 7abc02e and f59de5f.

📒 Files selected for processing (6)
  • dash-spv/src/client/config.rs
  • dash-spv/src/client/lifecycle.rs
  • dash-spv/src/main.rs
  • dash/src/network/constants.rs
  • dash/src/sml/llmq_type/mod.rs
  • dash/src/sml/llmq_type/network.rs

Comment on lines +226 to +233
/// Apply process-wide settings derived from this config. Idempotent for the
/// same values, returns an error if a conflicting setting was already applied.
pub(crate) fn apply_global_overrides(&self) -> Result<(), String> {
if let Some((size, threshold)) = self.llmq_devnet_params {
set_llmq_devnet_params(size, threshold).map_err(|e| e.to_string())?;
}
Ok(())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

apply_global_overrides idempotency contract doesn’t match implementation.

The method comment says same-value calls are idempotent, but OnceLock::set fails on every second call regardless of value. This can break creating multiple clients in one process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv/src/client/config.rs` around lines 226 - 233, The comment and
implementation disagree because set_llmq_devnet_params currently uses a OnceLock
that errors on any second set; change apply_global_overrides (and/or
set_llmq_devnet_params) so repeated calls with the same llmq_devnet_params
(size, threshold) are treated as idempotent: when self.llmq_devnet_params is
Some((size, threshold)) first check the existing stored params via the
OnceLock/get API (or equivalent accessor) and if a value is already set compare
it to (size, threshold) and return Ok(()) when they match, but return Err(...)
only when they conflict; only call OnceLock::set (or perform initialization)
when no value is present. Ensure you reference apply_global_overrides,
llmq_devnet_params, set_llmq_devnet_params and the OnceLock-backed storage when
making the change so multiple clients can apply identical overrides without
error.

Comment thread dash-spv/src/main.rs
Comment thread dash/src/network/constants.rs
Comment on lines +211 to +230
/// Runtime override for `LLMQ_DEVNET` params, matching Dash Core's `-llmqdevnetparams`.
static LLMQ_DEVNET_OVERRIDE: OnceLock<(u32, u32)> = OnceLock::new();

/// Override the `LLMQ_DEVNET` quorum size and threshold (matches Dash Core's
/// `-llmqdevnetparams=<size>:<threshold>`). May only be called once per process.
pub fn set_llmq_devnet_params(size: u32, threshold: u32) -> Result<(), &'static str> {
LLMQ_DEVNET_OVERRIDE.set((size, threshold)).map_err(|_| "LLMQ_DEVNET params already set")
}

/// Get the effective `LLMQ_DEVNET` params, applying any runtime override.
pub fn llmq_devnet_params() -> LLMQParams {
let mut params = LLMQ_DEVNET;
if let Some(&(size, threshold)) = LLMQ_DEVNET_OVERRIDE.get() {
params.size = size;
params.min_size = threshold;
params.threshold = threshold;
params.dkg_params.bad_votes_threshold = threshold;
}
params
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add focused tests for the new process-wide devnet override path.

Please add unit tests for: valid override application, invalid value rejection, and second-set behavior.

As per coding guidelines, "Write unit tests for new functionality".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash/src/sml/llmq_type/mod.rs` around lines 211 - 230, Add three tests that
exercise the process-wide override: write test_valid_override to call
set_llmq_devnet_params(size,threshold) with a valid pair and then assert
llmq_devnet_params() reflects size, min_size, threshold and
dkg_params.bad_votes_threshold; write test_invalid_override_rejected to call
set_llmq_devnet_params with invalid values (e.g., threshold > size or zero) and
assert it returns Err; write test_second_set_fails to call
set_llmq_devnet_params successfully then call it again and assert the second
call returns Err; because LLMQ_DEVNET_OVERRIDE is process-global, run each test
in its own process (spawn the test binary with --exact <test_name> or use
Command to re-exec the current test binary) so the OnceLock is fresh for each
case; reference functions/setter llmq_devnet_params, set_llmq_devnet_params and
static LLMQ_DEVNET_OVERRIDE and type LLMQParams when locating code to assert
fields.

Comment thread dash/src/sml/llmq_type/mod.rs Outdated
The `apply_global_overrides` doc claimed idempotent behavior, but `OnceLock::set` rejected every second call regardless of value. Constructing multiple `DashSpvClient` instances with the same devnet config in one process therefore failed on the second call.

Now the setter compares against the existing value and returns `Ok(())` when it matches, only erroring on a genuine conflict.

Addresses CodeRabbit review comment on PR #784
#784 (comment)
Add a single test that walks through valid initial set, idempotent reapplication of the same values, and rejection of a conflicting reapplication. The three checks share one test because `LLMQ_DEVNET_OVERRIDE` is a process-global `OnceLock` that cannot be reset between tests.

Addresses CodeRabbit review comment on PR #784
#784 (comment)
@xdustinface
Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
dash/src/sml/llmq_type/mod.rs (1)

217-223: ⚡ Quick win

Use a typed error for set_llmq_devnet_params

dash/src/sml/llmq_type/mod.rs exposes dash::sml::llmq_type::set_llmq_devnet_params(size, threshold) as a public API returning Result<(), &'static str>, and dash-spv/src/client/config.rs currently has to convert that via map_err(|e| e.to_string())?. Since the SML layer already uses thiserror (e.g., dash/src/sml/error.rs), add a small thiserror error type for this setter (conflict/AlreadySet) and return it instead of &'static str.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash/src/sml/llmq_type/mod.rs` around lines 217 - 223, Create a typed error
for set_llmq_devnet_params by adding a small thiserror-backed enum (e.g., enum
LlmqDevnetError { #[error("LLMQ_DEVNET params already set to a different
value")] AlreadySet }) in the sml error module or next to mod.rs, change the
signature of set_llmq_devnet_params(size: u32, threshold: u32) -> Result<(),
LlmqDevnetError>, and convert the existing string errors and map_err calls to
return LlmqDevnetError::AlreadySet where appropriate (referencing
LLMQ_DEVNET_OVERRIDE and the existing match arms/ set(...) map_err closure) so
callers can handle a concrete error type instead of &'static str.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dash/src/sml/llmq_type/mod.rs`:
- Around line 218-223: The current get()-then-set() sequence on
LLMQ_DEVNET_OVERRIDE is a TOCTOU race; instead attempt to set((size, threshold))
first and if set() fails read the stored value and accept it only if it equals
(size, threshold). Concretely, replace the match with: try
LLMQ_DEVNET_OVERRIDE.set((size, threshold)) and return Ok(()) on success; on
Err, call LLMQ_DEVNET_OVERRIDE.get() and compare the existing value to (size,
threshold) — return Ok(()) if equal, otherwise return the conflict Err. This
uses the atomic nature of set() as the first step and makes the idempotent path
safe under concurrent startup.

---

Nitpick comments:
In `@dash/src/sml/llmq_type/mod.rs`:
- Around line 217-223: Create a typed error for set_llmq_devnet_params by adding
a small thiserror-backed enum (e.g., enum LlmqDevnetError { #[error("LLMQ_DEVNET
params already set to a different value")] AlreadySet }) in the sml error module
or next to mod.rs, change the signature of set_llmq_devnet_params(size: u32,
threshold: u32) -> Result<(), LlmqDevnetError>, and convert the existing string
errors and map_err calls to return LlmqDevnetError::AlreadySet where appropriate
(referencing LLMQ_DEVNET_OVERRIDE and the existing match arms/ set(...) map_err
closure) so callers can handle a concrete error type instead of &'static str.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4a84584-e61a-4d9a-a16a-74ca4970ba63

📥 Commits

Reviewing files that changed from the base of the PR and between f59de5f and e3c8002.

📒 Files selected for processing (1)
  • dash/src/sml/llmq_type/mod.rs

Comment thread dash/src/sml/llmq_type/mod.rs
Comment thread dash-spv/src/client/config.rs Outdated
… tuple

`Option<(u32, u32)>` was unreadable at call sites — `with_llmq_devnet_params(13, 9)` is one transposition away from a silent bug. Introduce a named struct in `dashcore::sml::llmq_type` so the size/threshold meaning is explicit everywhere it crosses an API boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants