Skip to content

sweep: add durable descriptor-backed sweeps - #11067

Draft
sputn1ck wants to merge 2 commits into
lightningnetwork:masterfrom
sputn1ck:kon/descriptor-sweep-service
Draft

sweep: add durable descriptor-backed sweeps#11067
sputn1ck wants to merge 2 commits into
lightningnetwork:masterfrom
sputn1ck:kon/descriptor-sweep-service

Conversation

@sputn1ck

@sputn1ck sputn1ck commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Change Description

This PR adds a durable WalletKit service that watches for outputs described by
fixed native P2WSH or P2TR descriptors. Once one Miniscript branch can be
satisfied, the service gives the concrete output to lnd's existing
UtxoSweeper.

This separates contract discovery from contract satisfaction. An application
such as Loop can register an HTLC before it is funded, add a preimage after the
output confirms, or let a timelocked recovery branch mature. The existing
sweeper still batches compatible inputs, applies each input's fee budget, and
rebuilds the witness for every replacement transaction.

The lifecycle is:

  1. RegisterSweepDescriptor canonicalizes the descriptor, validates its public
    keys and lnd key bindings, derives the exact script, and persists the
    registration.
  2. The service watches for the exact script and expected value. It ignores
    wrong-value matches and rejects ambiguous exact matches.
  3. AddSweepDescriptorData can add an SHA256 preimage later. Block epochs also
    make height-based CLTV and block-based CSV branches available when they
    mature.
  4. The Miniscript planner selects one satisfiable branch. The service freezes
    that plan, its transaction constraints, its fee deadline, and, for P2TR,
    the selected Taproot leaf and control block.
  5. A descriptor-backed input.Input obtains fresh signatures and rebuilds its
    witness for every RBF candidate. UtxoSweeper then handles batching, fee
    bumping, publication, and confirmation.

P2TR script paths

P2TR registrations use script paths only. The Taproot key path is never offered
to the planner, so an external internal key may remain unbound while lnd signs
the keys used by one or more leaves.

The descriptor plan supplies the selected leaf script, leaf hash, Merkle root,
and control block. The service persists the selected leaf identity so a restart
cannot silently switch to another leaf with the same locktime constraints. It
also preserves the full parity of locator-derived keys for remote signers,
creates a fresh Schnorr signature with SigHashDefault for each RBF candidate,
and sends remote signers the real control block after verifying that it commits
the selected leaf to the watched output.

PendingSweeps reports these inputs as the new additive
DESCRIPTOR_TR = 44 witness type. Native P2WSH descriptor inputs retain
DESCRIPTOR_WSH = 43.

RPC and CLI surface

This PR adds:

  • RegisterSweepDescriptor and lncli wallet registersweepdescriptor;
  • AddSweepDescriptorData and lncli wallet addsweepdescriptordata; and
  • ListSweepDescriptors and lncli wallet listsweepdescriptors.

A registration includes the expected output value, chain birthday, required
confirmations, fee budget, deadline policy, and explicit mappings from
descriptor keys to lnd key locators. The list RPC reports the registration's
state, matched outpoint, sweep transaction, policy, and terminal error.

Persistence and recovery

The service stores registrations, preimages, scan cursors, discovered outputs,
frozen plans, deadlines, and lifecycle state in a versioned kvdb bucket. It
restores unfinished work after a restart and waits until the chain notifier and
sweeper are ready before resuming registrations.

Database transitions are store-first and copy-on-write. Transient notifier,
block-source, key-ring, database, and sweeper errors use bounded, quit-aware
retries; deterministic descriptor and satisfaction errors remain terminal.

Current scope

The first adapter deliberately accepts a narrow subset:

  • fixed-index native wsh(...) descriptors;
  • fixed-index tr(...) descriptors with Miniscript script paths;
  • raw compressed or x-only public keys, with explicit lnd bindings for keys
    that lnd must sign;
  • exact output-value matching;
  • late 32-byte SHA256 preimages; and
  • height-based CLTV and block-based CSV.

It does not use Taproot key-path spending or accept ranged or multipath
descriptors, extended or private keys, time-based locks, registration
cancellation, or pruning. min_confs defines source finality; after an input is
offered to the current sweeper, this service does not roll it back across a
deeper funding reorganization.

Stacked btcd dependency

This is a stacked draft on the btcd Miniscript and descriptor work in
btcd #2592 and
btcd #2568, plus the descriptor
extensions in
sputn1ck/btcd@877b61a3.
The extensions add late preimage lookup, potential-timelock discovery,
selected-path transaction constraints, frozen satisfaction replay, callback
isolation, and value-based metadata for the selected Taproot spend path.

The temporary go.mod replacement makes this draft reproducible without a
local workspace. It must be removed and the dependency repointed after the
btcd changes land upstream; this PR should not merge before then.

Steps to Test

Run the focused package, race, vet, and integration checks without a local Go
workspace replacement:

GOWORK=off go test -count=1 -tags=walletrpc \
  ./sweep/descriptorsweep ./lnwallet/rpcwallet \
  ./lnrpc/walletrpc ./cmd/commands .
GOWORK=off go test -race -count=1 \
  ./sweep/descriptorsweep ./lnwallet/rpcwallet
GOWORK=off go vet -tags=walletrpc \
  ./sweep/descriptorsweep ./lnwallet/rpcwallet \
  ./lnrpc/walletrpc ./cmd/commands .
GOWORK=off make itest icase=descriptor_sweep timeout=12m

The integration test runs four cases: P2WSH and P2TR, each through both sides
of this toy HTLC policy:

(success_key && sha256(preimage)) || (timeout_key && after(height))

The success cases register before funding, add the preimage after confirmation,
and observe a sweep without the future CLTV. The timeout cases supply no
preimage, mine to the CLTV height, and observe an automatic sweep with the exact
transaction locktime. The P2TR cases additionally verify the revealed leaf,
control-block depth, output commitment, exact pending outpoint, and
DESCRIPTOR_TR classification.

All commands above pass locally with GOWORK=off. The full custom linter,
release-note check, REST annotation check, generated-RPC build, and btcd
descriptor package tests also pass.

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative paths are included.
  • Bug fixes contain regression tests (not applicable: new feature).

Code Style and Documentation

  • The change is substantial.
  • The change follows the code documentation and 80-column guidelines.
  • The commits follow the ideal Git commit structure.
  • New logging uses an appropriate subsystem and level.
  • New lncli commands have matching RPC tags in the proto comments.
  • The change has a release-note entry.

@sputn1ck
sputn1ck force-pushed the kon/descriptor-sweep-service branch from cfb72f3 to 31e341c Compare August 13, 2026 12:51
@sputn1ck
sputn1ck force-pushed the kon/descriptor-sweep-service branch from cb4dbdf to 180c0be Compare August 13, 2026 13:47
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