feat: devnet support for dash-spv#784
Conversation
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`.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds 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. ChangesDevnet LLMQ Configuration and Runtime Overrides
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 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
|
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
dash-spv/src/client/config.rsdash-spv/src/client/lifecycle.rsdash-spv/src/main.rsdash/src/network/constants.rsdash/src/sml/llmq_type/mod.rsdash/src/sml/llmq_type/network.rs
| /// 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(()) | ||
| } |
There was a problem hiding this comment.
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.
| /// 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 | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
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)
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
dash/src/sml/llmq_type/mod.rs (1)
217-223: ⚡ Quick winUse a typed error for
set_llmq_devnet_params
dash/src/sml/llmq_type/mod.rsexposesdash::sml::llmq_type::set_llmq_devnet_params(size, threshold)as a public API returningResult<(), &'static str>, anddash-spv/src/client/config.rscurrently has to convert that viamap_err(|e| e.to_string())?. Since the SML layer already usesthiserror(e.g.,dash/src/sml/error.rs), add a smallthiserrorerror 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
📒 Files selected for processing (1)
dash/src/sml/llmq_type/mod.rs
… 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>
devnetin--network.Network::Devnetso storage can initialize.--devnet-name, embedded in the user agent asdevnet.devnet-<name>to pass Dash Core's devnet handshake check.Network::Devnetplatform quorum toLlmqtypeDevnetPlatform, matching Dash Core's default.ClientConfig::llmq_devnet_params(CLI--llmq-devnet-params=<size>:<threshold>) to mirror Dash Core's-llmqdevnetparamsoverride.Summary by CodeRabbit
New Features
Bug Fixes / Validation