Skip to content

feat(key-wallet-manager): WalletInterface::rewind_to_height with descendant cascade#169

Merged
xdustinface merged 14 commits into
devfrom
feat/wallet-rewind-to-height
May 26, 2026
Merged

feat(key-wallet-manager): WalletInterface::rewind_to_height with descendant cascade#169
xdustinface merged 14 commits into
devfrom
feat/wallet-rewind-to-height

Conversation

@xdustinface

Copy link
Copy Markdown
Owner

Summary

Phase 4b of #137. Builds on the Conflicted / Abandoned variants from #144 (merged in #168).

  • WalletInterface::rewind_to_height(&mut self, height) returning RewindResult { demoted_txids, conflicted_txids }. &mut self makes the lock contract compile-time.
  • WalletInterface::get_transaction(txid) for the auto-rebroadcast path in #146.
  • ManagedAccountTrait gains rewind_to_height with a fixed-point descendant cascade: walks transactions in reverse height order, demotes each record above the fork height, then iteratively demotes any in-account TxB whose inputs spend a demoted record's outputs.
  • UTXO and spent_outpoints rebuild from the rewind point (full rebuild for v1, optimise later if profiling justifies).
  • Rolls back last_processed_height and synced_height to min(h, current), per-wallet and aggregated.
  • Defense-in-depth: refuses rewind if h < last_applied_chain_lock.height, returns RewindError::BelowChainLockFloor.
  • Emits WalletEvent::Reorg { fork_height, demoted_txids, conflicted_txids }.
  • FFI exhaustive match arm in dash-spv-ffi/src/callbacks.rs handles the new event (full FFI callback surface deferred — TODO).
  • 7 new key-wallet-manager tests cover single demotion, descendant chains, IS retention, chainlock floor refuse, persisted rollback, get_transaction lookup, lock contract.

Deferred (per issue scope)

  • ISLock quorum re-validation: the wallet crate has no access to the masternode list engine that lives in dash-spv. Records currently in InstantSend(islock) are retained through the cut-by-height filter (the safer half is automatic). Demoting an IS record whose signing quorum no longer exists post-reorg requires SPV-side wiring driven via process_mempool_transaction(tx, None). TODO comment in wallet_info_interface.rs.
  • SPV-side SyncEvent::ChainReorgrewind_to_height plumbing lives in a follow-up — not in this PR's task list. Existing SPV reorg handlers don't call into the wallet today, so adding the method doesn't break downstream.
  • Self-conflict detection (mark Conflicted rather than Mempool when a new-chain block spends the same input as a demoted outgoing tx) deferred to #146. TODO comment in code.
  • Dedicated FFI callback for WalletEvent::Reorg scoped out — exhaustive match arm consumes/drops the event with a TODO so the C ABI keeps compiling.

Verification

  • cargo fmt --check clean
  • cargo clippy --all-targets -p key-wallet -p key-wallet-manager -p dash-spv -p dash-spv-ffi -- -D warnings clean
  • cargo test --lib -p key-wallet -p key-wallet-manager -p dash-spv all pass (487 + 53 + 538)
  • pre-commit run --all-files --hook-stage pre-push all hooks pass

Closes #145.

Add `demote_records_above`, `demote_record`, and (for the funds variant) `rebuild_utxos` so wallet-side reorg handling can demote above-cut records and rebuild the UTXO / spent-outpoint state from the surviving records. Mirrors the post-deserialization rebuild that the funds account's serde `Deserialize` impl already uses.

The keys variant only needs the context demotions since it carries no UTXO state. Both helpers leave records currently in `InstantSend`, `Conflicted`, or `Abandoned` contexts alone.
…dant cascade

Adds the wallet-level rewind orchestrator that consumes the new per-account demote / rebuild primitives. Validates the chainlock floor up front, runs an initial cut on every account, then drives a cross-account fixed-point loop that demotes any in-wallet record whose inputs spend an output of an already-demoted record. Finally rebuilds every funds account's UTXO state and rolls `last_processed_height` / `synced_height` back to `min(height, current)`.

`used_addresses` markers are intentionally preserved as monotonic state. The trait method returns a `RewindOutcome` so the manager-level emitter can fire a single atomic event per wallet. Self-conflict detection and ISLock-quorum re-validation are explicitly deferred to follow-ups with in-code TODOs.
…t_transaction`

Threads the wallet-side reorg path through the manager:

- new `RewindResult` and `RewindError` types
- `rewind_to_height` validates the chainlock floor across every managed wallet up front, then runs the per-wallet rewind and emits a single `WalletEvent::Reorg` per wallet whose state changed
- `get_transaction` lookup for the auto-rebroadcast path
- `WalletEvent::Reorg` variant with the partitioned demoted-vs-conflicted txid lists and the post-rewind balance

The `&mut self` receiver on `rewind_to_height` is the lock contract: implementations must not yield mid-rewind so concurrent mutators on the same trait surface cannot interleave with a reorg-driven demotion.

Mock implementations in `test_utils` get the minimum stubs needed to satisfy the trait.
Adds the exhaustive-match arm for the new `WalletEvent::Reorg`
variant. The C ABI does not yet carry a dedicated reorg callback,
so the arm logs intent via a TODO and drops the event. Wiring a
real callback is tracked separately.
Adds seven scenarios behind the new `WalletInterface`:

- single tx at H+1 demotes from `InBlock` to `Mempool` and emits a
  `WalletEvent::Reorg` with zero confirmed balance
- TxA at H+1, TxB spending TxA at H+2 cascades both
- IS-locked record is retained as `InstantSend` through the rewind
- rewind below the chainlock floor is rejected with the correct
  error fields and a rewind at the floor itself is allowed
- `last_processed_height` and `synced_height` roll back, and a
  rewind above the current height does not advance them forward
- `get_transaction` returns stored records and `None` for unknown
  txids
- Import `RewindError` at module top in `event_tests.rs` instead of using `crate::wallet_interface::RewindError` inline
- Use imported `WalletId` instead of `crate::WalletId` in `wallet_interface.rs`
- Replace em-dash prose separators in doc comments with `:` or `;`
- Remove caller-reference sentence from `spend_from` doc
- Rewrite `state_changed` field doc to describe the field, not its callers
@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Manki — Review complete

Planner (16s)
    feat · 809 lines · 4 agents
    review effort: medium · judge effort: high

Review — 12 findings
    ✅ Security & Safety — 2 (373s)
    ✅ Correctness & Logic — 2 (413s)
    ✅ Architecture & Design — 4 (124s)
    ✅ Testing & Coverage — 4 (217s)

Judge — 8 kept · 0 dropped (33s)
    kept: 1 blocker · 2 warning · 4 suggestion · 1 nitpick

Review metadata

Config:

  • Models: reviewer=claude-sonnet-4-6, judge=claude-opus-4-7
  • Review level: medium (auto, 809 lines)
  • Team: Security & Safety, Correctness & Logic, Architecture & Design, Testing & Coverage
  • Memory: disabled

Judge decisions:

  • ✓ Kept: "rebuild_utxos: wrong sort order leaves spent UTXOs in spendable set after rewind" (blocker, high confidence) — "Merged findings 1, 3, and 9 — same root cause. Three reviewers independently identified that sort_by_key on Option puts None (Mempool/IS) before Some(h), so a demoted child spending a still-confirmed parent processes first, inserts the parent outpoint into spent_outpoints, then the parent's later insert re-adds the UTXO unconditionally. update_utxos already guards this path; rebuild_utxos must mirror it. Impact: Critical (inflated balance, double-spend attempts), Likelihood: Probable (any reorg with pending mempool spends) → blocker."
  • ✓ Kept: "rebuild_utxos: is_trusted not restored, causing confirmed balance undercount after rewind" (suggestion, medium confidence) — "Conservative regression — never overstates funds, just shows trusted change as unconfirmed until reconfirmation. Single reviewer; downgrading from warning because the behavior is safe and the proposed fix requires adding new predicates (is_internal_address) that don't yet exist. Worth fixing but not shipping-blocking."
  • ✓ Kept: "Partial wallet rewind if inner BelowChainLockFloor fires after earlier wallets already mutated" (nitpick, high confidence) — "Merged with finding 6 — both observe the inner Err arm is unreachable today given the outer pre-check and &mut self. Real concern is latent (a future refactor could enable partial mutation), not active. Downgrading from warning because the current code is correct; the recommendation is hardening (collect-then-apply, or debug_assert_unreachable!)."
  • ✓ Kept: "Silent no-op default for rewind_to_height is dangerous for a state-critical operation" (warning, high confidence) — "Real design hazard: a correctness-critical mutation with a structurally-successful default that bypasses the state_changed gate and skips Reorg emission. Single reviewer but the impact (silent data inconsistency on any future implementor) justifies keeping it as warning. Trivial fix — drop the default body."
  • ✓ Kept: "Descendant cascade re-collects released outpoints from the entire already_demoted set each iteration instead of just the frontier" (suggestion, high confidence) — "Correct optimization observation; quadratic in spend-chain depth × accounts. Rewinds are rare and chain depth is typically small, so the practical impact is minimal. Suggestion is appropriate."
  • ✓ Kept: "demote_records_above and demote_record are duplicated verbatim across ManagedCoreKeysAccount and ManagedCoreFundsAccount" (suggestion, high confidence) — "Real maintainability concern — any correctness fix to the filter (e.g. handling InstantSend) must be made in two places. Standard dedup suggestion; not blocking."
  • ✓ Kept: "No test for UTXO restoration when parent is below cut and child is above cut" (warning, high confidence) — "This is the test that would catch the blocker finding above — the cascade test only asserts demotion, never balance/UTXO state. Keeping at warning: missing coverage on the primary correctness invariant of rebuild_utxos."
  • ✓ Kept: "No test verifying Reorg event is NOT emitted when rewind has no effect" (suggestion, medium confidence) — "Behavioral contract (state_changed gate) is untested but the consequence of regression is moderate (spurious Reorg event). Downgrading from warning — low-noise gap, easy follow-up."

Timing:

  • Parse: 2.8s
  • Review agents: 434.6s
  • Judge: 33.1s
  • Total: 470.5s

@codecov-commenter

codecov-commenter commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.64151% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.65%. Comparing base (1f1cc38) to head (7e7bc4d).

Files with missing lines Patch % Lines
...allet/managed_wallet_info/wallet_info_interface.rs 72.52% 25 Missing ⚠️
key-wallet-manager/src/events.rs 0.00% 9 Missing ⚠️
.../src/managed_account/managed_core_funds_account.rs 88.15% 9 Missing ⚠️
...t/src/managed_account/managed_core_keys_account.rs 65.38% 9 Missing ⚠️
dash-spv-ffi/src/callbacks.rs 0.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #169      +/-   ##
==========================================
- Coverage   73.77%   73.65%   -0.12%     
==========================================
  Files         325      325              
  Lines       74482    74694     +212     
==========================================
+ Hits        54948    55018      +70     
- Misses      19534    19676     +142     
Flag Coverage Δ
core 76.48% <ø> (ø)
ffi 46.32% <0.00%> (-1.21%) ⬇️
rpc 20.00% <ø> (ø)
spv 91.16% <ø> (+0.12%) ⬆️
wallet 71.52% <74.75%> (+0.05%) ⬆️
Files with missing lines Coverage Δ
key-wallet-manager/src/process_block.rs 93.16% <100.00%> (+0.06%) ⬆️
key-wallet-manager/src/wallet_interface.rs 14.28% <ø> (ø)
dash-spv-ffi/src/callbacks.rs 79.54% <0.00%> (-1.02%) ⬇️
key-wallet-manager/src/events.rs 65.25% <0.00%> (-2.88%) ⬇️
.../src/managed_account/managed_core_funds_account.rs 79.87% <88.15%> (+1.53%) ⬆️
...t/src/managed_account/managed_core_keys_account.rs 58.85% <65.38%> (+1.13%) ⬆️
...allet/managed_wallet_info/wallet_info_interface.rs 79.92% <72.52%> (-3.50%) ⬇️

... and 23 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

manki-review[bot]
manki-review Bot previously requested changes May 26, 2026

@manki-review manki-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Real blocker hiding in rebuild_utxos: sorting Mempool records (None < Some) before confirmed ones lets a demoted child re-spend a still-confirmed parent's UTXO that gets re-inserted unguarded — three independent reviewers landed on this same insert-without-spent-check bug, and there's no test that would catch it.

📊 8 findings (1 blocker, 2 warning, 4 suggestion, 1 nitpick) · 809 lines · 470s

Manki context
{
  "meta": {
    "prNumber": 169,
    "commitSha": "91bc227e4eb329455b23538c6a96bc670028a14c",
    "round": 1,
    "timestamp": "2026-05-26T08:37:59.309Z",
    "mankiVersion": "5.3.0",
    "cap": {
      "priorRoundCount": 0,
      "maxAutoRounds": 5,
      "skipCap": false,
      "forceReview": false,
      "bypassReason": "within_cap"
    },
    "trigger": {
      "event": "pull_request:opened",
      "sender": "xdustinface"
    }
  },
  "config": {
    "reviewLevel": "medium",
    "memoryEnabled": false,
    "reviewPasses": 1
  },
  "diff": {
    "lines": 809,
    "additions": 806,
    "deletions": 3,
    "filesReviewed": 10,
    "fileTypes": {
      ".rs": 9,
      ".toml": 1
    },
    "oversizedHandled": false,
    "perFile": [
      {
        "path": "dash-spv-ffi/src/callbacks.rs",
        "additions": 8,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/Cargo.toml",
        "additions": 1,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/event_tests.rs",
        "additions": 188,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/events.rs",
        "additions": 43,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/process_block.rs",
        "additions": 77,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/test_utils/mock_wallet.rs",
        "additions": 54,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/wallet_interface.rs",
        "additions": 78,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_funds_account.rs",
        "additions": 124,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_keys_account.rs",
        "additions": 41,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
        "additions": 192,
        "deletions": 1,
        "changeType": "modified"
      }
    ]
  },
  "models": {
    "planner": "claude-haiku-4-5",
    "reviewer": "claude-sonnet-4-6",
    "judge": "claude-opus-4-7",
    "dedup": "claude-haiku-4-5"
  },
  "planner": {
    "source": "planner",
    "used": true,
    "coreAgentInjections": [],
    "priorRoundEffortDowngrades": [],
    "teamSize": 4,
    "reviewerEffort": "medium",
    "judgeEffort": "high",
    "prType": "feat",
    "durationMs": 16077,
    "language": "rust",
    "context": "Dash HD wallet blockchain rewind",
    "agents": [
      {
        "name": "Security & Safety",
        "effort": "high"
      },
      {
        "name": "Correctness & Logic",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "effort": "medium"
      }
    ]
  },
  "reviewers": {
    "agents": [
      "Security & Safety",
      "Correctness & Logic",
      "Architecture & Design",
      "Testing & Coverage"
    ],
    "agentMetrics": [
      {
        "name": "Security & Safety",
        "findingsRaw": 2,
        "findingsKept": 2,
        "durationMs": 373125,
        "status": "success",
        "responseLength": 4503,
        "inputTokens": 3,
        "outputTokens": 19806,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Correctness & Logic",
        "findingsRaw": 2,
        "findingsKept": 1,
        "durationMs": 412972,
        "status": "success",
        "responseLength": 4205,
        "inputTokens": 3,
        "outputTokens": 23365,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "findingsRaw": 4,
        "findingsKept": 3,
        "durationMs": 124358,
        "status": "success",
        "responseLength": 6760,
        "inputTokens": 3,
        "outputTokens": 6820,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "findingsRaw": 4,
        "findingsKept": 3,
        "durationMs": 216621,
        "status": "success",
        "responseLength": 5804,
        "inputTokens": 3,
        "outputTokens": 12126,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      }
    ]
  },
  "judge": {
    "summary": "Real blocker hiding in rebuild_utxos: sorting Mempool records (None < Some) before confirmed ones lets a demoted child re-spend a still-confirmed parent's UTXO that gets re-inserted unguarded — three independent reviewers landed on this same insert-without-spent-check bug, and there's no test that would catch it.",
    "confidenceDistribution": {
      "high": 6,
      "medium": 2,
      "low": 0
    },
    "severityChanges": 8,
    "mergedDuplicates": 4,
    "durationMs": 33093,
    "retryCount": 0,
    "verdictReason": "required_present",
    "defensiveHardeningCount": 1,
    "verdictTrace": {
      "survivingBlockers": [
        {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "title": "rebuild_utxos: wrong sort order leaves spent UTXOs in spendable set after rewind",
          "fingerprint": "key-wallet/src/managed_account/managed_core_funds_account.rs:588:588:rebuild-utxos--wrong-sort-order-leaves-spent-UTXOs-in-spendable-set-after-rewind"
        }
      ],
      "novelWarnings": [],
      "unresolvedPriors": []
    },
    "openThreadsState": "empty",
    "openThreadCount": 0,
    "resolvedThreadIdCount": 0,
    "interRoundDiffState": "unknown",
    "interRoundDiffTruncated": false
  },
  "dedup": {
    "staticDropped": 0,
    "llmDropped": 0,
    "sameRoundLlmDropped": 4
  },
  "memory": {
    "loadStatus": "disabled"
  },
  "findings": {
    "count": 8,
    "severityCounts": {
      "blocker": 1,
      "warning": 2,
      "suggestion": 4,
      "nitpick": 1
    },
    "entries": [
      {
        "fingerprint": {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "lineStart": 588,
          "lineEnd": 588,
          "slug": "rebuild-utxos--wrong-sort-order-leaves-spent-UTXOs-in-spendable-set-after-rewind"
        },
        "severity": "blocker",
        "specialist": "Security & Safety",
        "suggestedFix": "In the \u0060rebuild_utxos\u0060 inner output loop, add the same guard already present in \u0060update_utxos\u0060:\n\n\u0060\u0060\u0060rust\nlet outpoint = OutPoint { txid, vout: vout as u32 };\nif self.spent_outpoints.contains(&outpoint) {\n    // Already spent by a previously-processed record in this rebuild.\n    continue;\n}\nlet txout",
        "title": "rebuild_utxos: wrong sort order leaves spent UTXOs in spendable set after rewind",
        "judgeNotes": "Merged findings 1, 3, and 9 — same root cause. Three reviewers independently identified that sort_by_key on Option<height> puts None (Mempool/IS) before Some(h), so a demoted child spending a still-confirmed parent processes first, inserts the parent outpoint into spent_outpoints, then the parent's later insert re-adds the UTXO unconditionally. update_utxos already guards this path; rebuild_utxos must mirror it. Impact: Critical (inflated balance, double-spend attempts), Likelihood: Probable (an",
        "judgeConfidence": "high",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "lineStart": 611,
          "lineEnd": 611,
          "slug": "rebuild-utxos--is-trusted-not-restored--causing-confirmed-balance-undercount-after-rewind"
        },
        "severity": "suggestion",
        "specialist": "Security & Safety",
        "suggestedFix": "Within the output loop in \u0060rebuild_utxos\u0060, reconstruct the trusted-output flag:\n\n\u0060\u0060\u0060rust\n// has_owned_input is true when any input of this tx spent a wallet UTXO\n// (which spent_outpoints already tracks from earlier records in this rebuild).\nlet has_owned_input = tx.input.iter()\n    .any(|i| self.sp",
        "title": "rebuild_utxos: is_trusted not restored, causing confirmed balance undercount after rewind",
        "judgeNotes": "Conservative regression — never overstates funds, just shows trusted change as unconfirmed until reconfirmation. Single reviewer; downgrading from warning because the behavior is safe and the proposed fix requires adding new predicates (is_internal_address) that don't yet exist. Worth fixing but not shipping-blocking.",
        "judgeConfidence": "medium",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet-manager/src/process_block.rs",
          "lineStart": 337,
          "lineEnd": 337,
          "slug": "Partial-wallet-rewind-if-inner--BelowChainLockFloor--fires-after-earlier-wallets-already-mutated"
        },
        "severity": "nitpick",
        "specialist": "Correctness & Logic",
        "suggestedFix": "Collect outcomes before mutating:\n\u0060\u0060\u0060rust\nlet mut outcomes: Vec<(WalletId, RewindOutcome)> = Vec::new();\nfor wallet_id in &wallet_ids {\n    let Some(info) = self.wallet_infos.get_mut(wallet_id) else { continue; };\n    match info.rewind_to_height(height) {\n        Ok(o) => outcomes.push((*wallet_id, ",
        "title": "Partial wallet rewind if inner \u0060BelowChainLockFloor\u0060 fires after earlier wallets already mutated",
        "judgeNotes": "Merged with finding 6 — both observe the inner Err arm is unreachable today given the outer pre-check and &mut self. Real concern is latent (a future refactor could enable partial mutation), not active. Downgrading from warning because the current code is correct; the recommendation is hardening (collect-then-apply, or debug_assert_unreachable!).",
        "judgeConfidence": "high",
        "reachability": "hypothetical",
        "reachabilityReasoning": "The outer pre-check iterates the same wallet set under the same &mut self with an identical predicate, so any wallet that passes the pre-check cannot fail the inner check today.",
        "tags": [
          "defensive-hardening"
        ],
        "originalSeverity": "suggestion"
      },
      {
        "fingerprint": {
          "file": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
          "lineStart": 218,
          "lineEnd": 218,
          "slug": "Silent-no-op-default-for-rewind-to-height-is-dangerous-for-a-state-critical-operation"
        },
        "severity": "warning",
        "specialist": "Architecture & Design",
        "suggestedFix": "// In the trait definition — remove the default body:\nfn rewind_to_height(\n    &mut self,\n    height: CoreBlockHeight,\n) -> Result<RewindOutcome, RewindRejection>;",
        "title": "Silent no-op default for rewind_to_height is dangerous for a state-critical operation",
        "judgeNotes": "Real design hazard: a correctness-critical mutation with a structurally-successful default that bypasses the state_changed gate and skips Reorg emission. Single reviewer but the impact (silent data inconsistency on any future implementor) justifies keeping it as warning. Trivial fix — drop the default body.",
        "judgeConfidence": "high",
        "reachability": "unknown",
        "reachabilityReasoning": "The trait is exported; reachability depends on whether any current or future implementor outside this PR's diff omits the override."
      },
      {
        "fingerprint": {
          "file": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
          "lineStart": 490,
          "lineEnd": 490,
          "slug": "Descendant-cascade-re-collects-released-outpoints-from-the-entire-already-demoted-set-each-iteration-instead-of-just-the-frontier"
        },
        "severity": "suggestion",
        "specialist": "Architecture & Design",
        "suggestedFix": "let mut frontier: Vec<Txid> = demoted_txids.clone();\nloop {\n    // Only collect outpoints released by the *previous* wave.\n    let released_outpoints: BTreeSet<OutPoint> = {\n        let mut set = BTreeSet::new();\n        for account in self.accounts.all_accounts() {\n            for txid in &frontier",
        "title": "Descendant cascade re-collects released outpoints from the entire already_demoted set each iteration instead of just the frontier",
        "judgeNotes": "Correct optimization observation; quadratic in spend-chain depth × accounts. Rewinds are rare and chain depth is typically small, so the practical impact is minimal. Suggestion is appropriate.",
        "judgeConfidence": "high",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet/src/managed_account/managed_core_keys_account.rs",
          "lineStart": 153,
          "lineEnd": 153,
          "slug": "demote-records-above-and-demote-record-are-duplicated-verbatim-across-ManagedCoreKeysAccount-and-ManagedCoreFundsAccount"
        },
        "severity": "suggestion",
        "specialist": "Architecture & Design",
        "suggestedFix": "// In ManagedAccountTrait or as a free function:\nfn demote_records_above(\n    transactions: &mut BTreeMap<Txid, TransactionRecord>,\n    height: CoreBlockHeight,\n) -> Vec<Txid> {\n    let to_demote: Vec<Txid> = transactions\n        .iter()\n        .filter_map(|(txid, record)| match record.context.bloc",
        "title": "demote_records_above and demote_record are duplicated verbatim across ManagedCoreKeysAccount and ManagedCoreFundsAccount",
        "judgeNotes": "Real maintainability concern — any correctness fix to the filter (e.g. handling InstantSend) must be made in two places. Standard dedup suggestion; not blocking.",
        "judgeConfidence": "high",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet-manager/src/event_tests.rs",
          "lineStart": 1395,
          "lineEnd": 1395,
          "slug": "No-test-for-UTXO-restoration-when-parent-is-below-cut-and-child-is-above-cut"
        },
        "severity": "warning",
        "specialist": "Testing & Coverage",
        "suggestedFix": "Add a test:\n\u0060\u0060\u0060rust\n#[tokio::test]\nasync fn test_rewind_restores_utxo_when_child_above_cut() {\n    let (mut manager, wallet_id, addr) = setup_manager_with_wallet();\n    let wallets = BTreeSet::from([wallet_id]);\n\n    // Parent confirmed at height 10.\n    let tx_a = create_tx_paying_to(&addr, 0xe0);\n",
        "title": "No test for UTXO restoration when parent is below cut and child is above cut",
        "judgeNotes": "This is the test that would catch the blocker finding above — the cascade test only asserts demotion, never balance/UTXO state. Keeping at warning: missing coverage on the primary correctness invariant of rebuild_utxos.",
        "judgeConfidence": "high",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet-manager/src/event_tests.rs",
          "lineStart": 1438,
          "lineEnd": 1438,
          "slug": "No-test-verifying-Reorg-event-is-NOT-emitted-when-rewind-has-no-effect"
        },
        "severity": "suggestion",
        "specialist": "Testing & Coverage",
        "suggestedFix": "Extend \u0060test_rewind_below_current_does_not_advance_heights_upward\u0060 (or add a new test) to subscribe before rewinding and assert the channel is empty afterwards:\n\u0060\u0060\u0060rust\nlet mut rx = manager.subscribe_events();\nmanager.rewind_to_height(1000).await.expect(\"rewind succeeds\");\nlet reorg_events: Vec<_> =",
        "title": "No test verifying Reorg event is NOT emitted when rewind has no effect",
        "judgeNotes": "Behavioral contract (state_changed gate) is untested but the consequence of regression is moderate (spurious Reorg event). Downgrading from warning — low-noise gap, easy follow-up.",
        "judgeConfidence": "medium",
        "reachability": "reachable"
      }
    ]
  },
  "usage": {
    "inputTokens": 26,
    "outputTokens": 65809,
    "totalTokens": 65835,
    "perStage": {
      "planner": {
        "inputTokens": 9,
        "outputTokens": 1417,
        "totalTokens": 1426
      },
      "reviewer": {
        "inputTokens": 12,
        "outputTokens": 62117,
        "totalTokens": 62129
      },
      "judge": {
        "inputTokens": 5,
        "outputTokens": 2275,
        "totalTokens": 2280
      }
    }
  },
  "verdict": "REQUEST_CHANGES",
  "recap": {
    "priorRoundCount": 0,
    "reclassifiedPriorCount": 0
  }
}

Reviewed commit 91bc227

Comment thread key-wallet/src/managed_account/managed_core_funds_account.rs
Comment thread key-wallet/src/managed_account/managed_core_funds_account.rs
Comment thread key-wallet-manager/src/process_block.rs
Comment thread key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
Comment thread key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
Comment thread key-wallet/src/managed_account/managed_core_keys_account.rs
Comment thread key-wallet-manager/src/event_tests.rs
Comment thread key-wallet-manager/src/event_tests.rs
…is_trusted`

Guards every UTXO insert against `spent_outpoints` so a record whose spending child sorts earlier (e.g. two records demoted to `Mempool` after a rewind, both with `None` heights) cannot resurrect a spent outpoint into the spendable set.

Restores `is_trusted` on self-send change outputs by detecting `has_owned_input` against the rebuild's own `spent_outpoints` and matching against the account's internal pool. Post-rewind self-send change outputs that demote to `Mempool` continue to bucket into `confirmed` rather than silently undercounting.

Addresses manki-review on PR [#169](#169) ([thread](#169 (comment)), [thread](#169 (comment))).
Removes the no-op default body of `WalletInfoInterface::rewind_to_height`. The default returned `Ok(RewindOutcome::default())` with `state_changed = false`, which silently suppresses the `WalletEvent::Reorg` event and leaves UTXOs unrebuilt — a correctness-critical mutation cannot be advisory. Any implementor that forgets to override would silently appear to succeed.

Replaces the descendant cascade's full `already_demoted` re-scan with a `frontier` that carries only the previous wave. Any newly reachable descendant must spend an output of that wave, so the prior `O(D²)` loop collapses to `O(D)` for the outpoint collection step. The `already_demoted` set is retained for the candidate-skip predicate.

Addresses manki-review on PR [#169](#169) ([thread](#169 (comment)), [thread](#169 (comment))).
…ys account

`ManagedCoreFundsAccount::demote_records_above` and `demote_record` were verbatim duplicates of the keys-account versions that routed through `self.keys.transactions_mut()`. Collapse the funds versions to one-line delegations so a future correctness fix to the demotion predicate (e.g. also skipping `InstantSend`) only needs to land on the keys account.

Addresses manki-review on PR [#169](#169) ([thread](#169 (comment))).
… on rewind

Extends `test_rewind_cascades_to_descendant_in_same_account` to assert the post-rewind balance equals exactly the child output value. The parent UTXO remains claimed by the child (still in `Mempool`), so only the child's output reaches the spendable set. This pins down the order-independence of `rebuild_utxos`: without the `spent_outpoints` insert guard the parent UTXO would resurrect into the spendable set whenever the child's txid sorts before the parent's.

Extends `test_rewind_below_current_does_not_advance_heights_upward` to subscribe to events before the no-op rewind and assert no `WalletEvent::Reorg` was emitted. This pins down the `state_changed` guard that suppresses spurious events.

Addresses manki-review on PR [#169](#169) ([thread](#169 (comment)), [thread](#169 (comment))).
@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Manki — Review complete

Planner (19s)
    feat · 828 lines · 4 agents
    review effort: medium · judge effort: medium

Review — 5 findings
    ✅ Security & Safety — 1 (203s)
    ✅ Correctness & Logic — 1 (141s)
    ✅ Architecture & Design — 1 (159s)
    ✅ Testing & Coverage — 2 (166s)

Judge — 3 kept · 2 dropped (29s)
    kept: 1 warning · 2 suggestion
    dropped: 1 warning · 1 suggestion

Review metadata

Config:

  • Models: reviewer=claude-sonnet-4-6, judge=claude-opus-4-7
  • Review level: medium (auto, 828 lines)
  • Team: Security & Safety, Correctness & Logic, Architecture & Design, Testing & Coverage
  • Memory: disabled

Judge decisions:

  • ✓ Kept: "rebuild_utxos: has_owned_input uses wrong signal — is_trusted always false after rewind" (warning, high confidence) — "Three independent reviewers converge: spent_outpoints holds inputs of already-processed records, not our owned outputs, so has_owned_input is false in a healthy chain and is_trusted never gets restored. Impact: confirmed balance undercount after rewind. Likelihood: Probable on any rewind with self-spend change. Consensus signal is strong."
  • ✗ Dropped: "rebuild_utxos: has_owned_input always false due to wrong data structure check" (ignore, high confidence) — "Duplicate of Finding 1 — merged."
  • ✗ Dropped: "rebuild_utxos: has_owned_input checks spent_outpoints instead of utxos, so is_trusted is always false" (ignore, high confidence) — "Duplicate of Finding 1 — merged."
  • ✓ Kept: "No test for 3-level cascade — frontier optimization is unverified past wave 1" (suggestion, medium confidence) — "Valid coverage gap: the frontier change only exercises wave 1 in tests, so a regression in frontier = new_demoted for wave 2+ would go undetected. Impact: Medium, Likelihood: Possible. Downgraded from warning since the cascade logic is simple and only one reviewer flagged it."
  • ✓ Kept: "Cascade balance assertion elides confirmed/unconfirmed split, leaving is_trusted fix unverified" (suggestion, medium confidence) — "Reasonable test improvement that would have caught Finding 1. Keep as suggestion per the reviewer's call."

Timing:

  • Parse: 3.2s
  • Review agents: 230.9s
  • Judge: 28.7s
  • Total: 262.8s

@manki-review manki-review Bot dismissed their stale review May 26, 2026 08:55

Superseded by new review

manki-review[bot]
manki-review Bot previously requested changes May 26, 2026

@manki-review manki-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sort-order blocker is resolved by the spent-outpoint guard, but three reviewers independently flag that has_owned_input checks spent_outpoints instead of utxos — so is_trusted is effectively never set after rewind and the confirmed/unconfirmed split remains broken. The new tests cover the 2-level cascade and no-op Reorg suppression but don't lock in the is_trusted semantics or a 3-level cascade.

📊 3 findings (1 warning, 2 suggestion) · 828 lines · 263s

Manki context
{
  "meta": {
    "prNumber": 169,
    "commitSha": "4128ef3a205d97df3f0d83b7365659a9d1a2519d",
    "round": 2,
    "timestamp": "2026-05-26T08:59:15.798Z",
    "mankiVersion": "5.3.0",
    "cap": {
      "priorRoundCount": 1,
      "maxAutoRounds": 5,
      "skipCap": false,
      "forceReview": false,
      "bypassReason": "within_cap"
    },
    "trigger": {
      "event": "pull_request:synchronize",
      "sender": "xdustinface"
    }
  },
  "config": {
    "reviewLevel": "medium",
    "memoryEnabled": false,
    "reviewPasses": 1
  },
  "diff": {
    "lines": 828,
    "additions": 825,
    "deletions": 3,
    "filesReviewed": 10,
    "fileTypes": {
      ".rs": 9,
      ".toml": 1
    },
    "oversizedHandled": false,
    "perFile": [
      {
        "path": "dash-spv-ffi/src/callbacks.rs",
        "additions": 8,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/Cargo.toml",
        "additions": 1,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/event_tests.rs",
        "additions": 209,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/events.rs",
        "additions": 43,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/process_block.rs",
        "additions": 77,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/test_utils/mock_wallet.rs",
        "additions": 54,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/wallet_interface.rs",
        "additions": 78,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_funds_account.rs",
        "additions": 118,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_keys_account.rs",
        "additions": 41,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
        "additions": 196,
        "deletions": 1,
        "changeType": "modified"
      }
    ]
  },
  "models": {
    "planner": "claude-haiku-4-5",
    "reviewer": "claude-sonnet-4-6",
    "judge": "claude-opus-4-7",
    "dedup": "claude-haiku-4-5"
  },
  "planner": {
    "source": "planner",
    "used": true,
    "coreAgentInjections": [],
    "priorRoundEffortDowngrades": [],
    "teamSize": 4,
    "reviewerEffort": "medium",
    "judgeEffort": "medium",
    "prType": "feat",
    "durationMs": 19156,
    "language": "rust",
    "context": "blockchain wallet with FFI bindings",
    "agents": [
      {
        "name": "Security & Safety",
        "effort": "high"
      },
      {
        "name": "Correctness & Logic",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "effort": "medium"
      }
    ]
  },
  "reviewers": {
    "agents": [
      "Security & Safety",
      "Correctness & Logic",
      "Architecture & Design",
      "Testing & Coverage"
    ],
    "agentMetrics": [
      {
        "name": "Security & Safety",
        "findingsRaw": 1,
        "findingsKept": 1,
        "durationMs": 203197,
        "status": "success",
        "responseLength": 2271,
        "inputTokens": 3,
        "outputTokens": 11596,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Correctness & Logic",
        "findingsRaw": 1,
        "findingsKept": 0,
        "durationMs": 141388,
        "status": "success",
        "responseLength": 1271,
        "inputTokens": 3,
        "outputTokens": 7886,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "findingsRaw": 1,
        "findingsKept": 0,
        "durationMs": 159188,
        "status": "success",
        "responseLength": 1980,
        "inputTokens": 3,
        "outputTokens": 8902,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "findingsRaw": 2,
        "findingsKept": 2,
        "durationMs": 166025,
        "status": "success",
        "responseLength": 3488,
        "inputTokens": 5,
        "outputTokens": 4629,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      }
    ]
  },
  "judge": {
    "summary": "The sort-order blocker is resolved by the spent-outpoint guard, but three reviewers independently flag that \u0060has_owned_input\u0060 checks \u0060spent_outpoints\u0060 instead of \u0060utxos\u0060 — so \u0060is_trusted\u0060 is effectively never set after rewind and the confirmed/unconfirmed split remains broken. The new tests cover the 2-level cascade and no-op Reorg suppression but don't lock in the \u0060is_trusted\u0060 semantics or a 3-level cascade.",
    "confidenceDistribution": {
      "high": 3,
      "medium": 2,
      "low": 0
    },
    "severityChanges": 5,
    "mergedDuplicates": 0,
    "durationMs": 28673,
    "retryCount": 0,
    "verdictReason": "novel_suggestion",
    "threadEvaluations": [
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4Yb",
        "status": "addressed",
        "reason": "Lines 594-600 now add \u0060if self.spent_outpoints.contains(&outpoint) { continue; }\u0060, an order-independent guard that prevents spent UTXOs from being resurrected regardless of sort order."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4Yt",
        "status": "not_addressed",
        "reason": "Line 615 sets \u0060utxo.is_trusted = has_owned_input && is_change\u0060, but \u0060has_owned_input\u0060 (line 581) checks \u0060self.spent_outpoints.contains(...)\u0060 — which holds inputs of already-processed records, not owned outputs. In a healthy chain this is always false, so \u0060is_trusted\u0060 is never set. Flagged by 3 independent reviewers."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4Y1",
        "status": "not_addressed",
        "reason": "process_block.rs:334-345 still returns Err on inner BelowChainLockFloor mid-iteration after earlier wallets have been mutated. No defensive change in the inter-round diff."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4ZI",
        "status": "addressed",
        "reason": "The trait default body was removed in wallet_info_interface.rs:224-228 — \u0060rewind_to_height\u0060 is now an unimplemented trait method, forcing each implementor to provide one."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4ZP",
        "status": "addressed",
        "reason": "The cascade loop now maintains a \u0060frontier: Vec<Txid>\u0060 initialized to \u0060demoted_txids.clone()\u0060 and reassigned to \u0060new_demoted\u0060 each iteration; the inner loop iterates \u0060&frontier\u0060 instead of \u0060&already_demoted\u0060 (lines 486-545)."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4Zc",
        "status": "addressed",
        "reason": "\u0060ManagedCoreFundsAccount::demote_records_above\u0060 and \u0060demote_record\u0060 now delegate to \u0060self.keys.demote_records_above(...)\u0060 / \u0060self.keys.demote_record(...)\u0060, removing the duplicated bodies."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4Zn",
        "status": "addressed",
        "reason": "The cascade test now asserts \u0060balance.confirmed() + balance.unconfirmed() == TX_AMOUNT / 2\u0060 (lines 1361-1369), verifying that exactly the child's output is in the spendable set after the parent is restored and re-spent by the still-present child."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6Eu4Z4",
        "status": "addressed",
        "reason": "\u0060test_rewind_below_current_does_not_advance_heights_upward\u0060 now subscribes to events before the no-op rewind, drains them, filters for \u0060WalletEvent::Reorg\u0060, and asserts the list is empty (lines 1449-1462)."
      }
    ],
    "verdictTrace": {
      "survivingBlockers": [],
      "novelWarnings": [
        {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "title": "rebuild_utxos: has_owned_input uses wrong signal — is_trusted always false after rewind",
          "fingerprint": "key-wallet/src/managed_account/managed_core_funds_account.rs:579:579:rebuild-utxos--has-owned-input-uses-wrong-signal---is-trusted-always-false-after-rewind"
        }
      ],
      "unresolvedPriors": []
    },
    "openThreadsState": "fetched",
    "openThreadCount": 8,
    "resolvedThreadIdCount": 0,
    "interRoundDiffState": "changed",
    "interRoundDiffBytes": 8227,
    "interRoundDiffTruncated": false
  },
  "dedup": {
    "staticDropped": 0,
    "llmDropped": 0,
    "durationMs": 1
  },
  "memory": {
    "loadStatus": "disabled"
  },
  "findings": {
    "count": 3,
    "severityCounts": {
      "blocker": 0,
      "warning": 1,
      "suggestion": 2,
      "nitpick": 0
    },
    "entries": [
      {
        "fingerprint": {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "lineStart": 579,
          "lineEnd": 579,
          "slug": "rebuild-utxos--has-owned-input-uses-wrong-signal---is-trusted-always-false-after-rewind"
        },
        "severity": "warning",
        "specialist": "Security & Safety",
        "suggestedFix": "// Option A – reverse sort so parents always precede children, then use utxos:\nrecords.sort_by_key(|r| r.context.block_info().map(|i| i.height()).unwrap_or(u32::MAX));\n// Then inside the loop, before inserting outputs:\nlet has_owned_input = tx.input.iter().any(|input| self.utxos.contains_key(&input.",
        "title": "rebuild_utxos: has_owned_input uses wrong signal — is_trusted always false after rewind",
        "judgeNotes": "Three independent reviewers converge: \u0060spent_outpoints\u0060 holds inputs of already-processed records, not our owned outputs, so \u0060has_owned_input\u0060 is false in a healthy chain and \u0060is_trusted\u0060 never gets restored. Impact: confirmed balance undercount after rewind. Likelihood: Probable on any rewind with self-spend change. Consensus signal is strong.",
        "judgeConfidence": "high",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet-manager/src/event_tests.rs",
          "lineStart": 1353,
          "lineEnd": 1353,
          "slug": "No-test-for-3-level-cascade---frontier-optimization-is-unverified-past-wave-1"
        },
        "severity": "suggestion",
        "specialist": "Testing & Coverage",
        "suggestedFix": "#[tokio::test]\nasync fn test_rewind_cascades_three_levels() {\n    let (mut manager, wallet_id, addr) = setup_manager_with_wallet();\n    let wallets = BTreeSet::from([wallet_id]);\n\n    let tx_a = create_tx_paying_to(&addr, 0xd0);\n    manager.process_block_for_wallets(&make_block(vec![tx_a.clone()], 0",
        "title": "No test for 3-level cascade — frontier optimization is unverified past wave 1",
        "judgeNotes": "Valid coverage gap: the frontier change only exercises wave 1 in tests, so a regression in \u0060frontier = new_demoted\u0060 for wave 2+ would go undetected. Impact: Medium, Likelihood: Possible. Downgraded from warning since the cascade logic is simple and only one reviewer flagged it.",
        "judgeConfidence": "medium",
        "reachability": "reachable"
      },
      {
        "fingerprint": {
          "file": "key-wallet-manager/src/event_tests.rs",
          "lineStart": 1362,
          "lineEnd": 1362,
          "slug": "Cascade-balance-assertion-elides-confirmed-unconfirmed-split--leaving--is-trusted--fix-unverified"
        },
        "severity": "suggestion",
        "specialist": "Testing & Coverage",
        "suggestedFix": "let balance = info.balance();\n// tx_b spends our own UTXO and pays change back to us: is_trusted must be set\n// by rebuild_utxos so the output lands in confirmed(), not unconfirmed().\nassert_eq!(\n    balance.confirmed(),\n    TX_AMOUNT / 2,\n    \"trusted mempool change must be credited to confirmed() ",
        "title": "Cascade balance assertion elides confirmed/unconfirmed split, leaving \u0060is_trusted\u0060 fix unverified",
        "judgeNotes": "Reasonable test improvement that would have caught Finding 1. Keep as suggestion per the reviewer's call.",
        "judgeConfidence": "medium",
        "reachability": "reachable"
      }
    ]
  },
  "usage": {
    "inputTokens": 28,
    "outputTokens": 36840,
    "totalTokens": 36868,
    "perStage": {
      "planner": {
        "inputTokens": 9,
        "outputTokens": 1793,
        "totalTokens": 1802
      },
      "reviewer": {
        "inputTokens": 14,
        "outputTokens": 33013,
        "totalTokens": 33027
      },
      "judge": {
        "inputTokens": 5,
        "outputTokens": 2034,
        "totalTokens": 2039
      },
      "dedup": {
        "inputTokens": 0,
        "outputTokens": 0,
        "totalTokens": 0
      }
    }
  },
  "verdict": "REQUEST_CHANGES",
  "recap": {
    "priorRoundCount": 1,
    "reclassifiedPriorCount": 0
  }
}

Reviewed commit 4128ef3

Comment thread key-wallet/src/managed_account/managed_core_funds_account.rs
Comment thread key-wallet-manager/src/event_tests.rs
Comment thread key-wallet-manager/src/event_tests.rs
@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Review skipped for 4128ef3 — a review has already been posted for this commit. Force a re-review:

  • Force review

…_utxos`

`has_owned_input` was checking `spent_outpoints`, which only sees
inputs of already-processed records. A mempool child sorted before
its block parent would observe an empty set, so `is_trusted` never
got restored and trusted change still landed in `unconfirmed` after
rewind.

Collect every owned outpoint from the active records in a pre-pass
and use that set for the check, decoupling the signal from sort
order.

Addresses manki review comment on PR #169
#169 (comment)
…restoration on rewind

Add `test_rewind_cascades_three_levels` so a regression that drops
descendants past the first wave of the frontier cascade is caught
by tests, and `test_rewind_restores_is_trusted_on_mempool_change`
to lock in the `has_owned_input` fix by paying change back to an
internal-pool address and asserting the demoted output lands in
`confirmed()` rather than `unconfirmed()`.

Addresses manki review comments on PR #169
#169 (comment)
#169 (comment)
@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Manki — Review complete

Planner (18s)
    feat · 913 lines · 4 agents
    review effort: medium · judge effort: medium

Review — 3 findings
    ✅ Security & Safety — 0 (164s)
    ✅ Correctness & Logic — 1 (270s)
    ✅ Architecture & Design — 0 (85s)
    ✅ Testing & Coverage — 2 (158s)

Judge — 1 kept · 0 dropped (81s)
    kept: 1 suggestion

Review metadata

Config:

  • Models: reviewer=claude-sonnet-4-6, judge=claude-opus-4-7
  • Review level: medium (auto, 913 lines)
  • Team: Security & Safety, Correctness & Logic, Architecture & Design, Testing & Coverage
  • Memory: disabled

Judge decisions:

  • ✓ Kept: "test_rewind_cascades_three_levels asserts txid demotion but not post-rewind balance" (suggestion, medium confidence) — "Impact: Low (test coverage gap), Likelihood: Possible (regression in deeper cascade UTXO arithmetic would slip past). The companion is_trusted test covers the rebuild correctness on a 2-level chain; adding a balance check here is a reasonable belt-and-suspenders but not required."

Timing:

  • Parse: 3.8s
  • Review agents: 296.1s
  • Judge: 81.0s
  • Total: 380.9s

@manki-review manki-review Bot dismissed their stale review May 26, 2026 09:09

Superseded by new review

manki-review[bot]
manki-review Bot previously requested changes May 26, 2026

@manki-review manki-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Previous three open threads all addressed: the owned-outpoints prepass replaces the bogus spent_outpoints signal, and two new tests cover the 3-level cascade and is_trusted restoration. One new suggestion about adding a balance assertion to the 3-level test — reasonable but not blocking.

Blocking unresolved review threads:

  • [Blocker] rebuild_utxos: wrong sort order leaves spent UTXOs in spendable set after rewind — key-wallet/src/managed_account/managed_core_funds_account.rs:588
  • [Warning] Silent no-op default for rewind_to_height is dangerous for a state-critical operation — key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs:218
  • [Warning] No test for UTXO restoration when parent is below cut and child is above cut — key-wallet-manager/src/event_tests.rs:1395
  • [Warning] rebuild_utxos: has_owned_input uses wrong signal — is_trusted always false after rewind — key-wallet/src/managed_account/managed_core_funds_account.rs:579

📊 1 findings (1 suggestion) · 913 lines · 381s

Manki context
{
  "meta": {
    "prNumber": 169,
    "commitSha": "746743103f281d67160ef4dd2e37f727d08938eb",
    "round": 3,
    "timestamp": "2026-05-26T09:15:17.189Z",
    "mankiVersion": "5.3.0",
    "cap": {
      "priorRoundCount": 2,
      "maxAutoRounds": 5,
      "skipCap": false,
      "forceReview": false,
      "bypassReason": "within_cap"
    },
    "trigger": {
      "event": "pull_request:synchronize",
      "sender": "xdustinface"
    }
  },
  "config": {
    "reviewLevel": "medium",
    "memoryEnabled": false,
    "reviewPasses": 1
  },
  "diff": {
    "lines": 913,
    "additions": 910,
    "deletions": 3,
    "filesReviewed": 10,
    "fileTypes": {
      ".rs": 9,
      ".toml": 1
    },
    "oversizedHandled": false,
    "perFile": [
      {
        "path": "dash-spv-ffi/src/callbacks.rs",
        "additions": 8,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/Cargo.toml",
        "additions": 1,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/event_tests.rs",
        "additions": 272,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/events.rs",
        "additions": 43,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/process_block.rs",
        "additions": 77,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/test_utils/mock_wallet.rs",
        "additions": 54,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/wallet_interface.rs",
        "additions": 78,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_funds_account.rs",
        "additions": 140,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_keys_account.rs",
        "additions": 41,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
        "additions": 196,
        "deletions": 1,
        "changeType": "modified"
      }
    ]
  },
  "models": {
    "planner": "claude-haiku-4-5",
    "reviewer": "claude-sonnet-4-6",
    "judge": "claude-opus-4-7",
    "dedup": "claude-haiku-4-5"
  },
  "planner": {
    "source": "planner",
    "used": true,
    "coreAgentInjections": [],
    "priorRoundEffortDowngrades": [],
    "teamSize": 4,
    "reviewerEffort": "medium",
    "judgeEffort": "medium",
    "prType": "feat",
    "durationMs": 18070,
    "language": "rust",
    "context": "Dash cryptocurrency wallet library",
    "agents": [
      {
        "name": "Security & Safety",
        "effort": "high"
      },
      {
        "name": "Correctness & Logic",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "effort": "medium"
      }
    ]
  },
  "reviewers": {
    "agents": [
      "Security & Safety",
      "Correctness & Logic",
      "Architecture & Design",
      "Testing & Coverage"
    ],
    "agentMetrics": [
      {
        "name": "Security & Safety",
        "findingsRaw": 0,
        "findingsKept": 0,
        "durationMs": 164161,
        "status": "success",
        "responseLength": 2,
        "inputTokens": 3,
        "outputTokens": 9211,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Correctness & Logic",
        "findingsRaw": 1,
        "findingsKept": 0,
        "durationMs": 269863,
        "status": "success",
        "responseLength": 2393,
        "inputTokens": 3,
        "outputTokens": 16053,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "findingsRaw": 0,
        "findingsKept": 0,
        "durationMs": 84702,
        "status": "success",
        "responseLength": 2,
        "inputTokens": 3,
        "outputTokens": 4561,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "findingsRaw": 2,
        "findingsKept": 1,
        "durationMs": 157718,
        "status": "success",
        "responseLength": 3758,
        "inputTokens": 3,
        "outputTokens": 9031,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      }
    ]
  },
  "judge": {
    "summary": "Previous three open threads all addressed: the owned-outpoints prepass replaces the bogus spent_outpoints signal, and two new tests cover the 3-level cascade and is_trusted restoration. One new suggestion about adding a balance assertion to the 3-level test — reasonable but not blocking.",
    "confidenceDistribution": {
      "high": 0,
      "medium": 1,
      "low": 0
    },
    "severityChanges": 1,
    "mergedDuplicates": 0,
    "durationMs": 80971,
    "retryCount": 0,
    "verdictReason": "prior_unaddressed",
    "threadEvaluations": [
      {
        "threadId": "PRRT_kwDOQSlaXs6EvRUs",
        "status": "addressed",
        "reason": "The diff replaces \u0060self.spent_outpoints.contains(...)\u0060 with \u0060owned_outpoints.contains(...)\u0060, where owned_outpoints is built via an up-front prepass over all record outputs filtered by \u0060account_type.contains_address\u0060, making has_owned_input a correct order-independent ownership signal (lines 580-603)."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6EvRVB",
        "status": "addressed",
        "reason": "New test \u0060test_rewind_cascades_three_levels\u0060 constructs a tx_a→tx_b→tx_c chain and asserts all three txids appear in demoted_txids, exercising wave 2 of the frontier propagation."
      },
      {
        "threadId": "PRRT_kwDOQSlaXs6EvRVK",
        "status": "addressed",
        "reason": "New test \u0060test_rewind_restores_is_trusted_on_mempool_change\u0060 asserts \u0060balance.confirmed() == TX_AMOUNT / 2\u0060 and \u0060balance.unconfirmed() == 0\u0060, directly verifying the is_trusted restoration in rebuild_utxos."
      }
    ],
    "verdictTrace": {
      "survivingBlockers": [],
      "novelWarnings": [],
      "unresolvedPriors": [
        {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "title": "rebuild_utxos: wrong sort order leaves spent UTXOs in spendable set after rewind",
          "fingerprint": "key-wallet/src/managed_account/managed_core_funds_account.rs:588:588:rebuild-utxos--wrong-sort-order-leaves-spent-UTXOs-in-spendable-set-after-rewind",
          "round": 1,
          "line": 588,
          "severity": "blocker"
        },
        {
          "file": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
          "title": "Silent no-op default for rewind_to_height is dangerous for a state-critical operation",
          "fingerprint": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs:218:218:Silent-no-op-default-for-rewind-to-height-is-dangerous-for-a-state-critical-operation",
          "round": 1,
          "line": 218,
          "severity": "warning"
        },
        {
          "file": "key-wallet-manager/src/event_tests.rs",
          "title": "No test for UTXO restoration when parent is below cut and child is above cut",
          "fingerprint": "key-wallet-manager/src/event_tests.rs:1395:1395:No-test-for-UTXO-restoration-when-parent-is-below-cut-and-child-is-above-cut",
          "round": 1,
          "line": 1395,
          "severity": "warning"
        },
        {
          "file": "key-wallet/src/managed_account/managed_core_funds_account.rs",
          "title": "rebuild_utxos: has_owned_input uses wrong signal — is_trusted always false after rewind",
          "fingerprint": "key-wallet/src/managed_account/managed_core_funds_account.rs:579:579:rebuild-utxos--has-owned-input-uses-wrong-signal---is-trusted-always-false-after-rewind",
          "round": 2,
          "line": 579,
          "severity": "warning"
        }
      ]
    },
    "openThreadsState": "fetched",
    "openThreadCount": 3,
    "resolvedThreadIdCount": 8,
    "interRoundDiffState": "changed",
    "interRoundDiffBytes": 5335,
    "interRoundDiffTruncated": false
  },
  "dedup": {
    "staticDropped": 0,
    "llmDropped": 2,
    "duplicateMatches": [
      {
        "droppedTitle": "test_rewind_cascades_three_levels tests height-filter, not cascade propagation",
        "matchedTitle": "Descendant cascade re-collects released outpoints from the entire already_demoted set each iteration instead of just the frontier",
        "matchType": "llm"
      },
      {
        "droppedTitle": "test_rewind_restores_is_trusted_on_mempool_change may not exercise the ordering-dependent failure path",
        "matchedTitle": "rebuild_utxos: is_trusted not restored, causing confirmed balance undercount after rewind",
        "matchType": "llm"
      }
    ],
    "durationMs": 68055
  },
  "memory": {
    "loadStatus": "disabled"
  },
  "findings": {
    "count": 1,
    "severityCounts": {
      "blocker": 0,
      "warning": 0,
      "suggestion": 1,
      "nitpick": 0
    },
    "entries": [
      {
        "fingerprint": {
          "file": "key-wallet-manager/src/event_tests.rs",
          "lineStart": 1371,
          "lineEnd": 1371,
          "slug": "test-rewind-cascades-three-levels-asserts-txid-demotion-but-not-post-rewind-balance"
        },
        "severity": "suggestion",
        "specialist": "Testing & Coverage",
        "suggestedFix": "let info = manager.get_wallet_info(&wallet_id).expect(\"wallet present\");\nlet balance = info.balance();\n// After rewind all three txs are in Mempool; only tx_a's output\n// is unspent (tx_b spends it, tx_c spends tx_b's output).\n// tx_c's output (TX_AMOUNT/4) is the surviving UTXO; tx_a's input\n// isn",
        "title": "test_rewind_cascades_three_levels asserts txid demotion but not post-rewind balance",
        "judgeNotes": "Impact: Low (test coverage gap), Likelihood: Possible (regression in deeper cascade UTXO arithmetic would slip past). The companion is_trusted test covers the rebuild correctness on a 2-level chain; adding a balance check here is a reasonable belt-and-suspenders but not required.",
        "judgeConfidence": "medium",
        "reachability": "reachable"
      }
    ]
  },
  "usage": {
    "inputTokens": 36,
    "outputTokens": 50222,
    "totalTokens": 50258,
    "perStage": {
      "planner": {
        "inputTokens": 9,
        "outputTokens": 1692,
        "totalTokens": 1701
      },
      "reviewer": {
        "inputTokens": 12,
        "outputTokens": 38856,
        "totalTokens": 38868
      },
      "judge": {
        "inputTokens": 5,
        "outputTokens": 694,
        "totalTokens": 699
      },
      "dedup": {
        "inputTokens": 10,
        "outputTokens": 8980,
        "totalTokens": 8990
      }
    }
  },
  "verdict": "REQUEST_CHANGES",
  "recap": {
    "priorRoundCount": 2,
    "reclassifiedPriorCount": 0
  }
}

Reviewed commit 7467431

Comment thread key-wallet-manager/src/event_tests.rs
@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Review skipped for 7467431 — a review has already been posted for this commit. Force a re-review:

  • Force review

…ascade

Adds a `confirmed() + unconfirmed()` assertion to `test_rewind_cascades_three_levels` so a `rebuild_utxos` regression that double-removes the intermediate `tx_b`'s output or resurrects a spent ancestor is caught by the test, not just frontier-propagation regressions.

Addresses manki-review review comment on PR #169
#169 (comment)
@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Manki — Review complete

Planner (34s)
    feat · 926 lines · 4 agents
    review effort: medium · judge effort: high

Review — 0 findings
    ✅ Security & Safety — 0 (41s)
    ✅ Correctness & Logic — 0 (83s)
    ✅ Architecture & Design — 0 (29s)
    ✅ Testing & Coverage — 0 (78s)

Judge — 0 kept · 0 dropped (5s)

Review metadata

Config:

  • Models: reviewer=claude-sonnet-4-6, judge=claude-opus-4-7
  • Review level: medium (auto, 926 lines)
  • Team: Security & Safety, Correctness & Logic, Architecture & Design, Testing & Coverage
  • Memory: disabled

Timing:

  • Parse: 5.0s
  • Review agents: 124.1s
  • Judge: 4.5s
  • Total: 133.6s

@manki-review manki-review Bot dismissed their stale review May 26, 2026 09:21

Superseded by new review

@manki-review manki-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No new findings from this round's reviewers — the prior cascade and is_trusted concerns appear settled and nothing new surfaced.

📊 0 findings (none) · 926 lines · 134s

Manki context
{
  "meta": {
    "prNumber": 169,
    "commitSha": "7e7bc4d1319a5e8a91675a27ee2432b02b648240",
    "round": 4,
    "timestamp": "2026-05-26T09:23:50.422Z",
    "mankiVersion": "5.3.0",
    "cap": {
      "priorRoundCount": 3,
      "maxAutoRounds": 5,
      "skipCap": false,
      "forceReview": false,
      "bypassReason": "within_cap"
    },
    "trigger": {
      "event": "pull_request:synchronize",
      "sender": "xdustinface"
    }
  },
  "config": {
    "reviewLevel": "medium",
    "memoryEnabled": false,
    "reviewPasses": 1
  },
  "diff": {
    "lines": 926,
    "additions": 923,
    "deletions": 3,
    "filesReviewed": 10,
    "fileTypes": {
      ".rs": 9,
      ".toml": 1
    },
    "oversizedHandled": false,
    "perFile": [
      {
        "path": "dash-spv-ffi/src/callbacks.rs",
        "additions": 8,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/Cargo.toml",
        "additions": 1,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/event_tests.rs",
        "additions": 285,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/events.rs",
        "additions": 43,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/process_block.rs",
        "additions": 77,
        "deletions": 1,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/test_utils/mock_wallet.rs",
        "additions": 54,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet-manager/src/wallet_interface.rs",
        "additions": 78,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_funds_account.rs",
        "additions": 140,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/managed_account/managed_core_keys_account.rs",
        "additions": 41,
        "deletions": 0,
        "changeType": "modified"
      },
      {
        "path": "key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs",
        "additions": 196,
        "deletions": 1,
        "changeType": "modified"
      }
    ]
  },
  "models": {
    "planner": "claude-haiku-4-5",
    "reviewer": "claude-sonnet-4-6",
    "judge": "claude-opus-4-7",
    "dedup": "claude-haiku-4-5"
  },
  "planner": {
    "source": "planner",
    "used": true,
    "coreAgentInjections": [],
    "priorRoundEffortDowngrades": [],
    "teamSize": 3,
    "reviewerEffort": "medium",
    "judgeEffort": "high",
    "prType": "feat",
    "durationMs": 33681,
    "language": "rust",
    "context": "Cryptocurrency wallet library (Dash)",
    "agents": [
      {
        "name": "Correctness & Logic",
        "effort": "high"
      },
      {
        "name": "Testing & Coverage",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "effort": "medium"
      }
    ]
  },
  "reviewers": {
    "agents": [
      "Security & Safety",
      "Correctness & Logic",
      "Architecture & Design",
      "Testing & Coverage"
    ],
    "agentMetrics": [
      {
        "name": "Security & Safety",
        "findingsRaw": 0,
        "findingsKept": 0,
        "durationMs": 41258,
        "status": "success",
        "responseLength": 2,
        "inputTokens": 3,
        "outputTokens": 1927,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      },
      {
        "name": "Correctness & Logic",
        "findingsRaw": 0,
        "findingsKept": 0,
        "durationMs": 82874,
        "status": "success",
        "responseLength": 2,
        "inputTokens": 3,
        "outputTokens": 4310,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      },
      {
        "name": "Architecture & Design",
        "findingsRaw": 0,
        "findingsKept": 0,
        "durationMs": 28823,
        "status": "success",
        "responseLength": 2,
        "inputTokens": 3,
        "outputTokens": 1314,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "medium"
      },
      {
        "name": "Testing & Coverage",
        "findingsRaw": 0,
        "findingsKept": 0,
        "durationMs": 77959,
        "status": "success",
        "responseLength": 2,
        "inputTokens": 3,
        "outputTokens": 3667,
        "retryCount": 0,
        "model": "claude-sonnet-4-6",
        "effort": "high"
      }
    ]
  },
  "judge": {
    "summary": "No new findings from this round's reviewers — the prior cascade and is_trusted concerns appear settled and nothing new surfaced.",
    "confidenceDistribution": {
      "high": 0,
      "medium": 0,
      "low": 0
    },
    "severityChanges": 0,
    "mergedDuplicates": 0,
    "durationMs": 4522,
    "retryCount": 0,
    "verdictReason": "only_nit_or_suggestion",
    "verdictTrace": {
      "survivingBlockers": [],
      "novelWarnings": [],
      "unresolvedPriors": []
    },
    "openThreadsState": "fetched",
    "openThreadCount": 0,
    "resolvedThreadIdCount": 12,
    "interRoundDiffState": "changed",
    "interRoundDiffBytes": 1174,
    "interRoundDiffTruncated": false
  },
  "dedup": {
    "staticDropped": 0,
    "llmDropped": 0
  },
  "memory": {
    "loadStatus": "disabled"
  },
  "findings": {
    "count": 0,
    "severityCounts": {
      "blocker": 0,
      "warning": 0,
      "suggestion": 0,
      "nitpick": 0
    },
    "entries": []
  },
  "usage": {
    "inputTokens": 26,
    "outputTokens": 14410,
    "totalTokens": 14436,
    "perStage": {
      "planner": {
        "inputTokens": 9,
        "outputTokens": 3086,
        "totalTokens": 3095
      },
      "reviewer": {
        "inputTokens": 12,
        "outputTokens": 11218,
        "totalTokens": 11230
      },
      "judge": {
        "inputTokens": 5,
        "outputTokens": 106,
        "totalTokens": 111
      }
    }
  },
  "verdict": "APPROVE",
  "recap": {
    "priorRoundCount": 3,
    "reclassifiedPriorCount": 0
  }
}

Reviewed commit 7e7bc4d

@manki-review

manki-review Bot commented May 26, 2026

Copy link
Copy Markdown

Review skipped for 7e7bc4d — a review has already been posted for this commit. Force a re-review:

  • Force review

@xdustinface xdustinface merged commit ac05e6f into dev May 26, 2026
42 of 43 checks passed
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