Skip to content

feat(dash-spv)!: trust quorums only below an anchored ChainLock - #1309

Closed
shumkov wants to merge 3 commits into
devfrom
shumkov/chainlock-anchoring
Closed

shumkov wants to merge 3 commits into
devfrom
shumkov/chainlock-anchoring

Conversation

@shumkov

@shumkov shumkov commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Why

dashpay/platform#3417 uses dash-spv's Verified quorum keys as the trust root for Platform proofs.

Verified only meant that DIP-6 checked a commitment against members taken from the list at its work block. Nothing tied that list to the chain the network locked:

What

The masternode engine holds a ChainLock anchor: the highest block it treats as final.

  • Root of trust:
    • mainnet and testnet: an embedded checkpoint past V20, each pinned in a test to the hash public block explorers report;
    • regtest and devnet, which have no checkpoints: the first ChainLock signed by a DIP-6 Verified quorum.
  • Anchored lists: a list at or below the anchor, on the header chain, is anchored.
  • Trust is checked on read, never stored. Anchoring can only remove trust, never add it. A header storage that can't be read grants no trust.
Consumer Rule
ChainLock acceptance (MasternodeListEngine::accept_chain_lock) DIP-8 selection at h - 8, with a signer that is anchored as active at the signing height. The anchored bound retires the quorums the mining windows in between may have retired. The anchored entry's key checks the signature. This replaces #1168's Verified signer check. An accepted ChainLock raises the anchor. A ChainLock is rejected only when an anchored list holding exactly the quorums active at its signing height lacks the signer. Otherwise it waits.
get_quorum_at_height (Platform) Returns the anchored entry. A quorum held only by lists above the anchor reads Skipped(MissingChainLock(list height, list block)), which Platform already treats as transient. No Platform change is needed.
InstantSend A lock verifies only if an anchored list holds the exact commitment that signed it, under the index its request id selects.
New latest_anchored_masternode_list() For security-relevant reads, such as Platform's ProUpServTx and voting code, which use latest_masternode_list() today.

Catch-up from a checkpoint or after a long gap. ChainLock quorums live about 2 days on mainnet and 1 day on testnet, and peers don't serve historical CLSIGs. The walk uses the ChainLock every block's coinbase carries instead (DIP-29: CbTx v3 bestCLSignature):

  1. It fetches the list a few ChainLock mining windows above the anchor: 1 window on mainnet, 11 on testnet.
  2. It then fetches the coinbases of the blocks just above that list, until an anchored quorum signed one of their ChainLocks.

How the walk behaves:

  • Proof: a coinbase counts only if the stored header of its block proves it, as for every list (fix(dash-spv)!: verify MnListDiff against its block's coinbase before applying #1200). A proven coinbase is the block's own, so whatever its ChainLock, the walk moves on to the next block.
  • Transport: it uses the network broker.
    • One request is wanted at a time. The walk declares it again on every tick, and releases it whenever it stops waiting for it.
    • A request is given up after 60 s. The walk then backs off: 30 s, doubling, at most 10 min.
  • Peers:
    • An answer the engine refuses keeps the request wanted, and that peer isn't heard again for that request. The broker asks another peer, so one peer can't stall the walk by refusing.
    • A refusal that blames the peer (fix(dash-spv)!: keep masternode sync live on bad or missing diffs #1258) also bans it.
    • Answers that two peers, or every peer there is, sent and that were refused without blame back the walk off, rather than get peer after peer kicked. That covers, for example, a quorum type the library doesn't know yet.
    • A gap on our side, such as a pruned base, re-plans the request once.
  • Alongside masternode sync:
    • The walk isn't a pending request of the diff pipeline, so tip updates and QRInfo go on while it runs.
    • The pipeline takes a diff both want, and the walk asks again.
    • Once synced, the walk stays below the newest list, and its lists alone don't end an initial sync.
  • Ordering: it runs before an initial sync's QRInfo, which waits for it for at most 30 s, counted again when peers come back after an outage. It also runs whenever a synced client's anchor falls behind.
  • Memory: pruning keeps the newest anchored list and the lists up to one ChainLock quorum lifetime above it, so a stuck anchor holds no more than that.
  • Persistence: each anchor raise is emitted as ChainLockReceived { validated: true }, and the ChainLock manager persists it.

Persistence:

  • Storage: the anchor goes under the new chainlock_anchor metadata key, together with its root. On restore it is accepted only if that root is one this network starts trust from.
  • Legacy key: best_chainlock is no longer trusted. It only acts as a fork floor while no anchor exists.
  • Crash ordering: headers flush to disk after the anchor does, so:
    • the anchor is only used while the stored header at its height is its block;
    • header sync refuses a header that conflicts with it;
    • once the stored chain holds the anchor's block, forks below it are ignored and handle_fork refuses to truncate below it. Until then the stored tip may sit on a branch a crash left below the anchor, so the chain through the anchored block must be free to replace that branch.

What anchoring guarantees

  • Quorums reported Verified come only from lists at or below a ChainLock that an anchored quorum signed. That chain of trust is rooted at a checkpoint on mainnet and testnet. With fix(dash-spv)!: verify MnListDiff against its block's coinbase before applying #1200, an anchored list's contents are what its block's coinbase commits to.
  • Trust assumption: every anchored ChainLock quorum has an honest signing threshold, which is DIP-8's own assumption.

What #1200 lacks compared with #1206 (closed), and why anchoring doesn't need it

Each of these fails closed on dev, by refusing the list. None is a forgery path:

Overlap with #1293

#1293 takes rotated quarter ChainLocks from the work blocks' coinbases. It changes DIP-24 member selection, not which quorums are trusted, so the two don't conflict in meaning.

Behaviour to expect

  • A new quorum is trusted one ChainLock later. It is anchored once a ChainLock signed by an already anchored quorum covers the block that mined it, because a ChainLock it signs itself can't vouch for it.
    • On mainnet, with 4 active ChainLock quorums and a ChainLock every block, that is usually the next block or two.
    • On regtest, with 2 active quorums, about half of the ChainLocks right after a DKG cycle come from the new quorum. The dashd tests therefore mine until the client's ChainLock height reaches what they need (mine_until_chain_locked_at_least).
  • After a restart, lists restored from the snapshot at or below the persisted anchor are anchored at once.

Not covered (follow-ups)

  • A mined branch above the anchor stalls anchoring. That is a denial of service, not a forgery. It needs ChainLock-driven fork recovery (dash-spv: protect fork recovery against malicious headers #1103, feat(dash-spv): block locator + staged fork detection #803).
  • The header chain on disk may not be linear after a crash (suspected, not reproduced). Header segments flush separately and the anchor is written at once, so after a reorg and a crash an older segment can still hold an abandoned branch. Persisting header segments in order before the anchor, or checking linkage at open, would close this.
  • Some peers can delay the walk. A peer that refuses and keeps its connection busy, or many sybils, can hold back each request until its deadline. A broker hint to avoid a peer (feat(dash-spv)!: let a network request avoid given peers #1212, parked) would close this.
  • Quorums based below the stored chain can't be aged against each other, so they are vouched for only within active mining windows of the stored chain's start.
  • DIP-6 statuses are keyed by quorum hash. A fake commitment with the same hash can only make a genuine quorum read as not Verified, which is a denial of service.
  • Platform follow-ups:
    • switch the ProUpServTx and voting reads to latest_anchored_masternode_list();
    • have is_ready also compare the ChainLock best_validated_height with the tip.
  • Possible follow-up: an operator escape hatch (ClientConfig::chainlock_anchor) for a network-wide ChainLock outage longer than a quorum's lifetime. Without it, fresh clients get no trust until a newer checkpoint ships.

Breaking

  • get_quorum_at_height reports quorums above the anchor as Skipped(MissingChainLock(..)).
  • ChainLocks need an anchored signer.
  • ChainLockReceived { validated: true } is also emitted during the catch-up walk, for blocks well below the tip.
  • The best ChainLock moves to the chainlock_anchor metadata key, so upgraded clients re-anchor from their checkpoint.
  • dash changes only a doc comment: it reuses LLMQEntryVerificationSkipStatus::MissingChainLock.

Commits

  1. feat(dash-spv)!: accept ChainLocks only from anchored quorums: the anchor, ChainLock acceptance, the walk, persistence and the fork floor.
  2. feat(dash-spv)!: report quorums above the anchor as not verified: get_quorum_at_height and latest_anchored_masternode_list.
  3. feat(dash-spv): verify IS locks only against anchored commitments

Tests

  • Red → green. Each test fails on its commit's parent:
    • a_chain_lock_signed_by_a_quorum_minted_above_the_anchor_is_not_accepted
    • a_quorum_no_verified_chain_lock_covers_is_not_reported_verified
    • a_lock_verifies_only_once_an_anchored_list_holds_its_quorum
  • Engine:
    • selection across a newly mined quorum;
    • same-hash substitution, at lookup and at ChainLock acceptance;
    • a list off the header chain below the anchor isn't anchored;
    • a ChainLock whose signer was mined past the newest anchored list waits, rather than being counted invalid;
    • pruning keeps the newest anchored list and the walk's reach, and nothing more;
    • the anchor waiting for its own block;
    • retired quorums;
    • a copied quorum set above the anchor;
    • quorums based below the stored chain;
    • an unverified quorum can't root trust on regtest;
    • no overflow on far-future heights;
    • coinbase ChainLocks, including a payload of another height and one reaching below genesis.
  • Walk, over stored header chains that prove the diffs, with a mocked broker:
    • planner invariants on every network;
    • the mainnet root at the checkpoint, and refusal of a full list its header doesn't prove (the peer is banned);
    • a carrier that can't anchor is followed by the next one, and a junk proof bans its peer;
    • a proven carrier whose ChainLock can't be final is passed over;
    • a list refused for its entries waits for another peer, and the refuser isn't heard again;
    • answers two peers, or the only peer, sent alike and refused back the walk off;
    • a gap of ours re-plans once, then backs off;
    • the deadline;
    • re-declaring on tick;
    • a diff the pipeline wants too, and releasing a target the pipeline wants from another base;
    • the trigger once synced, and staying below the newest list;
    • the walk's lists alone don't end an initial sync;
    • a reconnect drops the backoff and restarts the wait;
    • the walk started by header sync's completion and driven by tick;
    • the QRInfo release.
  • Elsewhere:
    • persistence and root filtering;
    • fork floor and anchor header check, including following a fork below an anchor whose block isn't stored;
    • adoption and persistence of the walk's ChainLocks;
    • InstantSend commitment equality, and a pending lock that verifies once a ChainLock anchors its quorum;
    • anchored lookups.

Evidence

Run at the tip, on origin/dev 2900e46e:

  • Lint and build checks: cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps, cargo +1.89 check --workspace --all-features, and typos 1.40.0 on the changed files. All are clean.
  • Tests: cargo test --workspace --all-features --no-fail-fast with the dashd environment set. 2854 passed, 0 failed. The 8 ignored tests are the repo's own #[ignore]s.
  • Each commit on its own: clippy (-p dash-spv --all-targets --all-features -D warnings) and the dash-spv lib tests pass: 696, 702 and 704.
  • Red → green: each regression test was run against the parent of its commit and failed there. The ChainLock test failed against origin/dev.

Review

  • Spec: four reviews on the original design (Core rules, security, liveness, simplicity), and three on this port to dev (correctness against dev, adversarial security, liveness).
  • Code: four more on the original implementation and two on its rebase. This port had four: correctness, security, liveness and tests.
  • All must-fixes are folded in. The residual risks are listed above.

🤖 Generated with Claude Code

PR Hygiene · 25d1557

  • Bots — coderabbitai ✓
  • Self-review — post /self-reviewed
  • Build green
  • Approvals
    • dash-spv (dash-spv/Cargo.toml, dash-spv/src/client/lifecycle.rs, dash-spv/src/client/queries.rs and 21 more) — QuantumExplorer or ZocoLini or xdustinface
    • files with no dedicated owner (dash/src/sml/llmq_entry_verification.rs) — QuantumExplorer or ZocoLini or xdustinface

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

Summary by CodeRabbit

  • New Features
    • Added ChainLock anchoring and catch-up, helping the client establish trusted chain history and resume synchronization.
    • InstantSend and quorum verification now require trusted, anchored quorum data.
  • Bug Fixes
    • Forks below a stored ChainLock anchor are rejected before wallet or storage state changes.
    • Pending ChainLocks and InstantLocks are retried as relevant headers and masternode data become available.

shumkov and others added 3 commits November 3, 2026 20:01
dash-spv's quorums become Platform's trust root (dashpay/platform#3417).
Since #1168 a ChainLock needs only a DIP-6 `Verified` signer, but DIP-6
checks a commitment only against members from the list at its work block,
and nothing tied that list to the chain the network locked. #1200 binds a
list to its block's coinbase, and DGW prices headers, but proof of work is
not finality: a miner with real hashrate can serve a short branch whose
coinbases commit to lists, and ChainLock quorums, of its own, which then
verify and sign ChainLocks for that branch.

The masternode engine now holds a ChainLock anchor: the highest block it
holds as final. Its root is a compiled-in checkpoint past V20 on mainnet
and testnet, or, on regtest and devnet, which have no checkpoints, the
first ChainLock a DIP-6 verified quorum signs. A list at or below the
anchor, on the header chain, is anchored. Trust is checked when read,
never stored:

- A ChainLock is accepted only when its DIP-8 signing quorum is anchored
  as active at `height - 8`, and then raises the anchor.
  - The anchored entry's key verifies it, never one from a list above the
    anchor.
  - Selection reads a list holding exactly the quorums active at the
    signing height, or both lists around it when a quorum was mined
    between them.
  - An anchored list below vouches only for those of its quorums the
    mining windows in between cannot have retired. Equal quorum sets count
    only between anchored lists.
- A ChainLock that waits for an anchored signer stays pending instead of
  counting as invalid, and is retried as the anchor moves. One is rejected
  only when an anchored list holding exactly the quorums active at its
  signing height lacks its signer.

The anchor walks up from an old checkpoint, or across a long gap, through
the ChainLocks block coinbases carry (DIP-29):
- The masternode manager fetches the list a few ChainLock mining windows
  above the anchor, then the coinbases of the blocks just above it, until
  an anchored quorum signed one of them.
- A coinbase counts only if the stored header of its block proves it, as
  for every list the engine builds (#1200). A proven coinbase is the
  block's own, so whatever its ChainLock the walk moves on.
- Requests go through the broker, one at a time, declared again on every
  tick and released once the walk stops waiting for them. A request is
  given up after 60 s, and the walk backs off: 30 s, doubling, at most
  10 min.
- An answer the engine refuses keeps the request wanted, and its peer is
  not heard again for it, so the broker asks another peer; a refusal that
  blames the peer (#1258) also bans it. Once two peers, or every peer
  there is, sent answers refused without blame, the walk backs off
  rather than have peer after peer kicked. A gap of ours, such as a
  pruned base, plans the request again once.
- The walk is not a pending request of the masternode pipeline, so tip
  updates and QRInfo go on while it runs. Once synced it stays below the
  newest list, and its lists alone do not end an initial sync.
- It runs before an initial sync's QRInfo, which waits for it for at most
  30 s, and whenever a synced client's anchor falls behind.
- Its ChainLocks are announced as validated `ChainLockReceived` events,
  so the ChainLock manager persists them.
- Pruning keeps the newest anchored list and the lists up to a ChainLock
  quorum's lifetime above it, which the walk builds on.

The anchor is persisted under the new `chainlock_anchor` key with its
root, and one whose root this network does not start trust from is
ignored. A best ChainLock persisted before this change is no longer
trusted, and only keeps forks from going below it while there is no
anchor. Since headers flush later than the anchor is persisted:
- an anchor is used only while the stored header at its height is its
  block;
- header sync refuses a header that conflicts with it;
- forks below it are refused, and `handle_fork` refuses to truncate below
  it, once the stored chain holds its block. Until then the stored tip
  may lie on a branch a crash left below it, which the chain through the
  anchored block must be free to replace.

Every embedded checkpoint trust may start at is pinned to the hash public
block explorers report for its height.

Residual: a quorum whose base block is below the stored chain, as the
checkpoint's are, cannot be aged against its like. It is vouched for only
while fewer than `active` mining windows passed above the stored chain's
start.

A new ChainLock quorum is trusted once a ChainLock an already anchored
quorum signs covers the block that mined it. On regtest, with two active
ChainLock quorums, about half the ChainLocks right after a DKG cycle come
from the new one, so the `dashd_masternode` tests mine until the client's
ChainLock height reaches what they need.

Test would have caught this in CI:
`a_chain_lock_signed_by_a_quorum_minted_above_the_anchor_is_not_accepted`
fails on the parent, where the forged ChainLock is accepted
(`validated: true`), and passes here.

BREAKING CHANGE: ChainLocks need an anchored signer.
`ChainLockReceived { validated: true }` is also emitted for the anchor's
walk, for blocks well below the tip. The best ChainLock moves to the
`chainlock_anchor` metadata key, so upgraded clients re-anchor from their
checkpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Platform verifies its proofs with the quorums `get_quorum_at_height`
reports `Verified` (dashpay/platform#3417). `Verified` only meant that
DIP-6 checked a commitment against members from the list at its work
block, so a quorum a peer put in lists on a branch of its own, which DIP-6
then checks against those same lists, read `Verified` too.

`get_quorum_at_height` now returns the entry of the newest anchored list
holding the quorum: at or below both the height and the ChainLock anchor
(the best verified ChainLock, or the checkpoint trust starts from), on the
header chain. It keeps its DIP-6 status, so anchoring only takes trust
away. A quorum only lists above the anchor hold reads
`Skipped(MissingChainLock(list height, list block))`, which Platform
already treats as transient. The entry always comes from the anchored
list, never from a list above the anchor, which may carry another
commitment under the same hash.

New `DashSpvClient::latest_anchored_masternode_list`, the newest list at
or below the anchor, for decisions that rely on a masternode's entry.
`latest_masternode_list` is unchanged, and its doc now says it is not
anchored.

`dash` changes only the doc comment of `MissingChainLock`, which now also
covers a list no anchor covers yet.

Test would have caught this in CI:
`a_quorum_no_verified_chain_lock_covers_is_not_reported_verified` fails on
the parent, where the quorum reads `Verified`, and passes here.

BREAKING CHANGE: `get_quorum_at_height` reports a quorum that only lists
above the ChainLock anchor hold as `Skipped(MissingChainLock(..))`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An IS lock is checked against the quorums of its rotation cycle, which the
engine stores from QRInfo `last_commitment_per_index`. No coinbase commits
to that set, so a QRInfo peer can store a cycle under a genuine cycle hash
with commitments of its own, and lists no ChainLock covers yet can carry
another commitment under a genuine quorum's hash.

A lock now verifies only when the commitment its request id selects sits
under that quorum index in the cycle and is the very commitment an
anchored list holds, at or below the ChainLock anchor, on the header
chain. Until then the lock stays pending, within its TTL, and pending
locks are checked again on every `ChainLockReceived { validated: true }`,
since an accepted ChainLock may have anchored their quorum.

Test would have caught this in CI:
`a_lock_verifies_only_once_an_anchored_list_holds_its_quorum` fails on the
parent, where the lock verifies with no anchored list holding its quorum,
and passes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shumkov shumkov self-assigned this Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The PR adds ChainLock anchor trust to the masternode engine and uses it in ChainLock acceptance, header fork checks, quorum queries, and InstantLock validation. It also adds a masternode catch-up walk that requests list diffs and ChainLock carriers to advance the anchor.

Changes: ChainLock Anchor and Synchronization

Layer / File(s) Summary
Anchor trust and anchored lookups
dash-spv/Cargo.toml, dash-spv/src/sml_engine.rs, dash-spv/src/sml_engine/anchor.rs, dash-spv/src/sml_engine/helpers.rs, dash-spv/src/client/queries.rs, dash-spv/src/client/lifecycle.rs, dash-spv/src/validation/chainlock.rs, dash/src/sml/llmq_entry_verification.rs
The engine tracks trust roots and ChainLock anchors, checks list and quorum coverage against stored headers, and retains lists around the anchor. Client queries distinguish anchored quorums from quorums in uncovered lists. Chain truncation below a usable anchor is rejected.
Anchor persistence and lock acceptance
dash-spv/src/sync/chainlock/*, dash-spv/src/sync/block_headers/manager.rs, dash-spv/src/sync/events.rs, dash-spv/src/sync/mod.rs
ChainLock acceptance delegates to the masternode engine. The manager persists a best lock with its anchor root and restores it only when the engine accepts the anchor. Header validation checks persisted anchor hashes and applies a stored anchor as a fork floor. Accepted-lock events trigger adoption and pending-lock retries.
Masternode anchor catch-up
dash-spv/src/sync/masternodes/*
The catch-up walk plans list and carrier requests from an eligible trust root, handles accepted carriers, and tracks retries, peer refusals, and backoff. Masternode synchronization starts and advances the walk and routes diffs it expects.
InstantLock quorum anchoring
dash-spv/src/sync/instantsend/*, dash-spv/src/validation/instantlock.rs, dash-spv/tests/dashd_masternode/*
InstantLock checks require an anchored quorum entry that exactly matches the cycle entry. Initial and pending checks share this verification path. Integration tests use a helper that mines until the requested ChainLock height is reached.

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant MasternodesSyncManager
  participant CatchUp
  participant NetworkManager
  participant MasternodeListEngine
  participant HeaderStorage
  MasternodesSyncManager->>CatchUp: begin_catch_up or tick_catch_up
  CatchUp->>NetworkManager: request list diff or carrier
  NetworkManager-->>CatchUp: return MnListDiff
  CatchUp->>MasternodeListEngine: accept carrier ChainLock
  MasternodeListEngine->>HeaderStorage: check locked block hash
  HeaderStorage-->>MasternodeListEngine: return stored header result
  MasternodeListEngine-->>CatchUp: return verdict and updated anchor
  CatchUp-->>MasternodesSyncManager: emit validated ChainLock event
Loading

Possibly related issues

  • dashpay/rust-dashcore issue 1264: InstantLock validation now requires an exact quorum entry from an anchored list, addressing the issue's concern about trusting cycle commitments without checking them against anchored-list data.

Suggested reviewers: xdustinface, zocolini, quantumexplorer

Merge Risk: Minimal · up to 25d15

This change roots ChainLock and quorum trust in an anchor and adds a catch-up walk to advance it. No actionable merge-blocking issue was found in the supplied evidence. Known follow-ups are documented by the author and do not block merging.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: restricting quorum trust to ChainLocks with an anchored trust root. The breaking-change marker is appropriate for the new trust behavior and …
Docstring Coverage Passed Docstring coverage is 84.44% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentati…
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.
Full details: Title check

Explanation

The title clearly and concisely describes the main change: restricting quorum trust to ChainLocks with an anchored trust root. The breaking-change marker is appropriate for the new trust behavior and API changes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 84.44% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 90 functions across 24 files. (1 skipped: 1 unsupported.)

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2900e46 and 25d1557.

Files selected for processing: 25
  • dash-spv/Cargo.toml
  • dash-spv/src/client/lifecycle.rs
  • dash-spv/src/client/queries.rs
  • dash-spv/src/sml_engine.rs
  • dash-spv/src/sml_engine/anchor.rs
  • dash-spv/src/sml_engine/helpers.rs
  • dash-spv/src/sync/block_headers/manager.rs
  • dash-spv/src/sync/chainlock/manager.rs
  • dash-spv/src/sync/chainlock/mod.rs
  • dash-spv/src/sync/chainlock/sync_manager.rs
  • dash-spv/src/sync/events.rs
  • dash-spv/src/sync/instantsend/manager.rs
  • dash-spv/src/sync/instantsend/sync_manager.rs
  • dash-spv/src/sync/masternodes/catch_up.rs
  • dash-spv/src/sync/masternodes/manager.rs
  • dash-spv/src/sync/masternodes/mod.rs
  • dash-spv/src/sync/masternodes/sync_manager.rs
  • dash-spv/src/sync/mod.rs
  • dash-spv/src/validation/chainlock.rs
  • dash-spv/src/validation/instantlock.rs
  • dash-spv/tests/dashd_masternode/helpers.rs
  • dash-spv/tests/dashd_masternode/tests_chainlock.rs
  • dash-spv/tests/dashd_masternode/tests_instantsend.rs
  • dash-spv/tests/dashd_masternode/tests_sync.rs
  • dash/src/sml/llmq_entry_verification.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Nov 3, 2026
@codecov

codecov Bot commented Nov 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.77419% with 90 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.65%. Comparing base (2900e46) to head (25d1557).
⚠️ Report is 2 commits behind head on dev.

Files with missing lines Patch % Lines
dash-spv/src/sync/masternodes/catch_up.rs 96.54% 41 Missing ⚠️
dash-spv/src/sml_engine/anchor.rs 96.28% 38 Missing ⚠️
dash-spv/src/sync/chainlock/manager.rs 97.17% 5 Missing ⚠️
dash-spv/src/sync/block_headers/manager.rs 96.72% 4 Missing ⚠️
dash-spv/src/sync/instantsend/manager.rs 99.00% 1 Missing ⚠️
dash-spv/src/sync/masternodes/sync_manager.rs 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1309      +/-   ##
==========================================
+ Coverage   82.13%   82.65%   +0.51%     
==========================================
  Files         290      292       +2     
  Lines       76201    78882    +2681     
==========================================
+ Hits        62588    65200    +2612     
- Misses      13613    13682      +69     
Flag Coverage Δ
core 81.92% <ø> (ø)
ffi 60.37% <ø> (ø)
rpc 52.10% <ø> (ø)
spv 94.26% <96.77%> (+0.37%) ⬆️
wallet 83.07% <ø> (ø)
Files with missing lines Coverage Δ
dash-spv/src/client/lifecycle.rs 94.77% <100.00%> (+0.99%) ⬆️
dash-spv/src/client/queries.rs 93.33% <100.00%> (+55.49%) ⬆️
dash-spv/src/sml_engine.rs 97.43% <100.00%> (+<0.01%) ⬆️
dash-spv/src/sml_engine/helpers.rs 100.00% <100.00%> (ø)
dash-spv/src/sync/chainlock/sync_manager.rs 87.50% <100.00%> (-12.50%) ⬇️
dash-spv/src/sync/events.rs 94.23% <ø> (ø)
dash-spv/src/sync/instantsend/sync_manager.rs 100.00% <100.00%> (ø)
dash-spv/src/sync/masternodes/manager.rs 97.15% <100.00%> (+0.21%) ⬆️
dash-spv/src/validation/chainlock.rs 97.41% <ø> (-2.59%) ⬇️
dash-spv/src/validation/instantlock.rs 94.16% <100.00%> (-0.15%) ⬇️
... and 7 more

... and 1 file with indirect coverage changes

@PastaPastaPasta

Copy link
Copy Markdown
Member

I have concerns about this design. A few concerns:

  1. The attack you're trying to prevent assumes an attacker can mine 10-30 blocks at mainnet diff. Not small cost.
  2. This would result in code that cannot sync after a chain locks outage. This is opposite to the assumptions of core/spv where the liveness of PoW keeps things moving even through an outage.
  3. If there's an outage you can only fix it by a release. That's a problem.
  4. If we wanted to do this it'd be better accomplished by using the actual DIP 175 approach and querying it via DAPI instead of reimplementing a parallel implementation of it in rust-dashcore sync logic.

@shumkov

shumkov commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, agreed. Points 2 and 3 are real: once a ChainLock outage outlasts a ChainLock quorum's lifetime, the anchor can't move without a release, and we deliberately left the operator escape hatch out. And with DIP 175's verifier already in Platform (dashpay/platform#5521), finality for Platform's quorum keys belongs there rather than in a second implementation inside dash-spv's sync. Closing this. For dashpay/platform#3417 we'll rely on #1161 and #1200 for the SPV-synced keys, and add DIP 175 proofs on the Platform side.

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

Labels

waiting-self-review Waiting for the author to post /self-reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants