Skip to content

staticaddr: support multiple receive and change addresses - #1218

Open
hieblmi wants to merge 20 commits into
masterfrom
multi-address-core
Open

staticaddr: support multiple receive and change addresses#1218
hieblmi wants to merge 20 commits into
masterfrom
multi-address-core

Conversation

@hieblmi

@hieblmi hieblmi commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

This is PR 1 of 3 in the Static Address multi-address stack.

Static Address previously treated one legacy/root address as the owner of every
deposit. This PR introduces fresh receive and operation-specific change
addresses while preserving the address parameters that own each deposit.

Loop-ins and withdrawals can consequently spend deposits received across
multiple derived addresses, with each input signed and proven using its actual
address parameters.

Key Changes

  • Derive fresh static receive and change addresses from dedicated key families.
  • Persist each deposit's owning static-address parameters and restore them
    across restarts.
  • Backfill pre-migration deposits and fractional loop-ins to the legacy/root
    address.
  • Create cooperative signing sessions from each deposit's actual keys.
  • Send per-deposit address proofs to the server for loop-ins and withdrawals.
  • Generate and persist operation-specific change addresses for fractional
    loop-ins and partial withdrawals.
  • Rebuild and maintain an active address index during startup.
  • Import only missing wallet scripts and narrowly recognize duplicate-import
    errors for the expected Taproot output key.
  • Resolve requested funding addresses through the active script index instead
    of reconstructing and scanning every persisted address.
  • Track expiry sweeps by the spent deposit outpoint so unrelated transactions
    paying the same destination script cannot finalize a deposit.
  • Add loop static deposit support for creating and optionally funding a fresh
    address through lnd SendCoins.
  • Include each deposit's receiving address in RPC and CLI listings.

RPC and Compatibility Changes

NewStaticAddress now derives a fresh receive address when no existing address
is supplied through send_coins_request.addr. Callers must not assume repeated
requests are idempotent or return the same address.

Because address creation mutates wallet and database state, the RPC permission
changes from swap:read to swap:execute. Operators using custom scoped
macaroons must rebake them accordingly.

StaticAddressSummaryResponse.static_address remains populated with the
legacy/root address for wire compatibility, but is deprecated and must not be
treated as the current receive address. Call NewStaticAddress to derive a
fresh address.

Database and Recovery

The database now records:

  • The static-address owner of every deposit.
  • The generated change address associated with a fractional loop-in.

Migrations backfill existing records using the legacy/root address. Store
reconstruction restores deposit ownership and loop-in and withdrawal change
metadata after restart.

Stacked PRs

  1. This PR — multi-address persistence, derivation, signing, RPC, and CLI core.
  2. [staticaddr/withdraw: harden replacement monitoring #1215 — withdrawal replacement hardening](https://github.com/lightninglabs/loop/
    pull/1215)
    validates and follows the transactions that actually replace multi-address
    withdrawals.
  3. #1217 — loop-in HTLC recovery
    persists the confirmed HTLC output so recovery can rebuild the exact
    server-published transaction.

The address startup and lookup hardening previously isolated in
#1214 has been folded into
this PR, and #1214 is now closed.

Testing

  • go test ./...
  • go vet ./...
  • go test -race ./staticaddr/address ./loopd
  • make docs-check
  • Commit-message lint across the complete PR range
  • git diff --check

Release Notes

Release notes document the new address behavior, RPC permission change,
deprecated summary field, migration compatibility, and address lookup
hardening.

hieblmi added 12 commits August 27, 2026 10:44
Reserve separate key families for static receive and change addresses.
This keeps derived keys out of the legacy static-address and HTLC key
streams.
Associate every deposit with the static address parameters that created
it. This lets restored deposits recover the correct script and signing
keys instead of assuming the legacy root address.
Create receive and change addresses from locally derived client keys
while reusing the server key and expiry from the legacy seed. Persist,
import, and activate each script before returning it to callers.

Rebuild the active address index on startup and serialize issuance
without blocking address reads. Import only scripts missing from lnd,
and accept duplicate-import errors only when they identify the expected
Taproot output key.
Look up each newly discovered wallet UTXO by script and persist the
matching active-address parameters on the deposit. Reject unknown
scripts before allocating the timeout sweep address.

Use the per-deposit parameters when constructing the FSM, sign
descriptor, and unilateral expiry sweep so derived-address recovery uses
its owning script and key.
Register timeout-sweep confirmations by destination script instead of
the originally published txid. This lets recovery detect an RBF
replacement after restart with a stale txid.
Construct each cooperative MuSig2 session from the address parameters
stored on its deposit. This prepares loop-ins and withdrawals to sign
inputs belonging to different derived static addresses.

Clean up sessions created before a later setup failure. Reject duplicate
deposit outpoints to avoid leaking signer state.

Validate transaction inputs, session handles, and nonce counts before
signing so malformed responses fail before any signer operation.
Map every selected outpoint to the static address descriptor that
derived its deposit and include those descriptors in loop-in requests.
This lets the server validate mixed-address inputs independently of
request order.
Include the derivation key for every withdrawal input in the server
request. This lets the server validate and sign withdrawals that combine
deposits from multiple derived addresses.
Join restored deposits with their persisted static-address rows and
hydrate pre-migration deposits from the legacy root address. Return
complete ownership metadata from deposit reads and use each deposit's
own expiry during loop-in selection.
Associate fractional loop-ins with their operation-specific static
change address so recovery restores the descriptor needed to reconstruct
signed transactions. Backfill legacy fractional swaps to the original
address.
Create a fresh static address for fractional loop-in change and send its
descriptor to the server. Reconstruct signed HTLCs with the persisted
parameters and verify cooperative batch change by output script.
Multi-address loop-ins sign and construct transactions from the
parameters attached to each deposit and their dedicated change address.
The legacy root address fields therefore became write-only, but
populating them could still abort signing, sweep handling, or recovery
when the root lookup failed.

Remove those fields and lookups, select the FSM from the protocol
version persisted with the swap, and set that version before
constructing new state machines. Keep the root-parameter lookup used by
autoloop expiry calculation and add regression coverage for recovery and
unsupported persisted versions.
@hieblmi hieblmi mentioned this pull request Aug 28, 2026
1 task
@hieblmi hieblmi self-assigned this Aug 28, 2026
@hieblmi
hieblmi requested a review from starius August 28, 2026 13:56
@lightninglabs lightninglabs deleted a comment from lightninglabs-gateway Bot Aug 28, 2026
@hieblmi
hieblmi force-pushed the multi-address-core branch from 1179815 to eb2cb02 Compare August 28, 2026 15:01
Create a fresh static address for partial-withdrawal change and identify
it in the confirmed transaction through its active change-family script,
without assuming output order or count. Record withdrawn and change
amounts by script identity.

Keep all withdrawal outputs in the PSBT without separate signing
metadata while preserving full-withdrawal behavior.
Let loop static deposit create and fund a fresh receive address through
lnd SendCoins. Validate funding arguments before address creation and
require explicit confirmation unless --force is set, including for
non-interactive and first-use deposits.

Allow NewStaticAddress RPC callers to fund a requested existing static
address by resolving it through the active script index. Expose the
nested request through the client RPC, require swap:execute permission,
and cover the CLI new-address and daemon existing-address funding paths.
Regenerate RPC and CLI documentation.
Include the owning static address in every deposit RPC response and CLI
listing. Users can distinguish deposits created by different receive and
change addresses without reconstructing scripts externally.

Calculate blocks until expiry from each deposit owner instead of the
legacy root address, and reject deposits whose owning parameters are
missing. Centralize deposit response conversion and update generated RPC
artifacts, regression coverage, and command replay fixtures.
The CLI previously recognized an uninitialized static-address seed by
searching arbitrary gRPC error text. Any wrapping or wording change
could suppress the L402 backup warning before a user funded a newly
derived address.

Map ErrNoStaticAddress to codes.NotFound at the RPC boundary and
classify that status in the CLI. Retain compatibility with older daemons
only for an exact Unknown-status message, avoiding the broad substring
match, and cover both sides with regression tests.
A static-address account can now receive deposits across multiple
derived addresses, so the singular summary field can no longer describe
the current receive address. Removing or repurposing field 1 would break
existing clients.

Keep the wire value as the legacy/root derivation address, formally
deprecate it, document the expiry as the shared CSV delay, and direct
CLI users to derive a fresh receive address. Rename the server locals to
make the compatibility behavior explicit and regenerate protobuf and
Swagger artifacts.
Cover per-deposit address ownership and operation-specific change
outputs across the shared SQL persistence boundary. Reconstruct the
deposit, loop-in, and withdrawal stores to verify ownership and change
metadata survive restart.
Document fresh receive-address derivation, lazy seed initialization,
funding-address lookup hardening, and the swap:execute permission
required by address creation. Regenerate the CLI, gRPC, Swagger, and
man-page documentation and add feature, breaking-change, and recovery
release notes.
@hieblmi
hieblmi force-pushed the multi-address-core branch from eb2cb02 to 7aa2e74 Compare August 28, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant