Skip to content

feat(key-wallet): explicit UTXO override for asset-lock funding#850

Closed
lklimek wants to merge 2 commits into
devfrom
feat/key-wallet-asset-lock-explicit-utxo-override
Closed

feat(key-wallet): explicit UTXO override for asset-lock funding#850
lklimek wants to merge 2 commits into
devfrom
feat/key-wallet-asset-lock-explicit-utxo-override

Conversation

@lklimek

@lklimek lklimek commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Why this PR exists

  • Problem: ManagedWalletInfo::build_asset_lock_with_signer hardcodes TransactionBuilder::set_funding(...) — automatic coin selection over all of an account's locally-indexed UTXOs. That local index can retain entries flagged is_confirmed = true that are, in reality, already spent on-chain. This is an out-of-order rescan/event-processing defect (block-processing order isn't guaranteed height-order within a batch — see #649), independent of the separately-fixed #836 (unconfirmed-funds guard, which does not cover this case).
  • What breaks without it: A downstream consumer (dash-evo-tool) hit this concretely and traced it forensically: key-wallet selects a UTXO that Insight confirms was spent ~54 days / ~31,000 confirmations earlier. The resulting asset-lock transaction is a double-spend from the network's perspective — no honest peer relays it, so it never gets an IS-lock or ChainLock, and the caller's wait_for_proof burns its full timeout waiting on a proof that can never arrive. Reproduced against real chain state (not a test fixture artifact) three times, with three different stale UTXOs, including after a from-genesis rescan and with a fresh real deposit in the wallet.
  • Scope: fixing the root cause (the local UTXO index not being reliably invalidated when the network genuinely spends an output) is a separate, deeper fix in managed_core_funds_account.rs::update_utxos. This PR does not attempt that — it gives callers who have independently verified a UTXO against real chain state (e.g. via their own oracle) a way to force the builder to use it, bypassing the (sometimes-wrong) local auto-selector for asset-lock funding specifically. A companion dashpay/platform PR consumes this once merged.

What was done

  • build_asset_lock_with_signer gains an override_utxo: Option<Utxo> parameter. None reproduces today's behavior byte-for-byte (fully backward compatible).
  • New TransactionBuilder::set_funding_with_inputs(funds_acc, acc, inputs) — a sibling of set_funding that preserves its two side effects (change-address derivation, reservation-set population/double-spend guard) but sources candidate inputs from the caller instead of funds_acc.utxos. A raw add_inputs() swap was considered and rejected: it does neither of those, which would silently produce no change output and no local double-spend guard on the override input.
  • When override_utxo is Some but not is_confirmed || is_instantlocked, the builder eagerly returns a new AssetLockError::OverrideInputNotFinal instead of silently filtering it out downstream — the caller gets an explicit signal, not a mysteriously-ignored input.
  • impl std::error::Error for AssetLockError added (was missing), enabling the companion platform-side PR to wrap it via #[from] instead of erasing it to a string.
  • The soft-wallet build_asset_lock path is untouched — it has no override consumer.

Testing

  • cargo test -p key-wallet: 546 passed, 0 failed (10 new override cases + 1 builder pool-isolation unit test; TDD — red demonstrated before green).
  • cargo clippy -p key-wallet --all-features --all-targets -- -D warnings: clean.
  • cargo fmt: clean.

Breaking changes

build_asset_lock_with_signer's signature gains a parameter. Its only known consumer (dashpay/platform's rs-platform-wallet) is updated in the companion PR to pass None everywhere except the new override path.

Checklist

  • Tests added/updated
  • cargo clippy --all-features --all-targets -- -D warnings clean
  • cargo fmt clean
  • Companion dashpay/platform PR (pin bump) — opens once this is reviewed

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

Give callers a way to fund an asset lock from exactly one pre-verified
UTXO, bypassing the automatic coin selector — without disturbing the
default path any existing caller exercises. The root-cause index defect
(out-of-order-rescan retaining spent coins as confirmed) is worked
around here, not fixed.

- `TransactionBuilder::set_funding_with_inputs`: sibling of `set_funding`
  that sources candidates from the caller instead of `funds_acc.utxos`,
  keeping change-address derivation and the reservation capture/skip
  guard verbatim. The candidate pool is isolated structurally — a
  poisoned or absent index entry can neither leak in nor gate selection.
- `build_asset_lock_with_signer` gains `override_utxo: Option<Utxo>`.
  `None` reproduces today's behavior byte-for-byte. `Some` makes the
  override the sole candidate; a non-final override is rejected eagerly
  with the new typed `AssetLockError::OverrideInputNotFinal` rather than
  silently filtered into a generic insufficient-funds error.
- `impl std::error::Error for AssetLockError` with `source()` delegating
  to the wrapped `BuilderError`, so downstream `#[from]` chains reach the
  structured `InsufficientFunds { available, required }` fields.

Tests: 10 override cases plus a builder-level pool-isolation unit test.
The soft `build_asset_lock` path is deliberately untouched (no consumer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9383f526-1577-4afb-947d-22136054e39b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/key-wallet-asset-lock-explicit-utxo-override

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

❤️ Share

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

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.92671% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.48%. Comparing base (49c243a) to head (d42081e).

Files with missing lines Patch % Lines
...c/wallet/managed_wallet_info/asset_lock_builder.rs 96.66% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #850      +/-   ##
==========================================
+ Coverage   73.35%   73.48%   +0.13%     
==========================================
  Files         324      324              
  Lines       72933    73351     +418     
==========================================
+ Hits        53502    53905     +403     
- Misses      19431    19446      +15     
Flag Coverage Δ
core 76.94% <ø> (ø)
ffi 45.64% <ø> (+0.01%) ⬆️
rpc 20.00% <ø> (ø)
spv 90.52% <ø> (-0.03%) ⬇️
wallet 73.47% <96.92%> (+0.49%) ⬆️
Files with missing lines Coverage Δ
.../wallet/managed_wallet_info/transaction_builder.rs 86.32% <100.00%> (+0.67%) ⬆️
...c/wallet/managed_wallet_info/asset_lock_builder.rs 92.23% <96.66%> (+4.09%) ⬆️

... and 6 files with indirect coverage changes

…d + tests

Address QA LOW-severity polish on the explicit-UTXO-override path:

- SEC-001: reject an override whose `address` and `txout.script_pubkey`
  disagree with the new typed `AssetLockError::OverrideInputScriptMismatch`,
  eagerly in `build_asset_lock_with_signer` before any signing. Signing
  derives the key from `address` but signs over `script_pubkey`; the guard
  stops an account-derivable address from ever producing a signature over
  an unrelated script — defense-in-depth against a signing oracle if this
  path is ever exposed beyond the trusted native-Rust caller.
- QA-001: add a committed regression test that a foreign-account override
  (address not derivable by the funding account) fails closed at signing
  with `BuilderError::SigningFailed` and strands no reservation — locks in
  the previously-incidental safety property.
- RUST-003: trim `set_funding_with_inputs`'s rustdoc to the relaxed
  public-API length cap without losing the isolation or locking contract.

Deferred by design (not addressed): SEC-003 (distinct error for
locked/immature-coinbase override — already fails closed) and RUST-001
(share the reservation/change tail between `set_funding` variants).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lklimek lklimek closed this Jul 8, 2026
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.

1 participant