Skip to content

backup: add immutable backups for HD-style recovery#1173

Draft
hieblmi wants to merge 35 commits into
lightninglabs:masterfrom
hieblmi:loop-backup
Draft

backup: add immutable backups for HD-style recovery#1173
hieblmi wants to merge 35 commits into
lightninglabs:masterfrom
hieblmi:loop-backup

Conversation

@hieblmi

@hieblmi hieblmi commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

only the last commit is relevant

This PR introduces an encrypted, immutable backup for each paid L402/static-address generation.
The backup preserves the paid L402 token and the stable static-address metadata required to
reconstruct legacy, receive, and change addresses from the corresponding lnd seed.

The backup intentionally does not store mutable address indexes or deposit state. Instead, it
establishes the foundation for a follow-up PR that will implement HD-wallet-style recovery by
deterministically deriving addresses, scanning the chain, and rebuilding Loop’s local state.

hieblmi added 30 commits July 12, 2026 09:14
Retain static-address deposits as soon as lnd reports the UTXO, even
when the output is still unconfirmed. Store the first confirmation
height once the output confirms.

Derive confirmation heights from the current wallet view because lnd
reports confirmation counts instead of first-confirmation heights.
The first block epoch is consumed before recovered deposit FSMs exist.
Replay that startup height after recovery so already-expired deposits
can run expiry handling immediately after restart.
Treat lnd's wallet view as the source of spendable static-address
outpoints while keeping historical deposit records in the DB. Reconcile
active FSMs against the current wallet view, reactivate known deposits
that reappear, and hide stale Deposited records from the visible
deposit set.
Build list and summary responses from tracked deposit records instead
of raw wallet UTXOs so RPC clients see the manager availability state.
Split unconfirmed value from confirmed deposited value in summaries,
and reject manual loop-in quotes for selected deposits that are not
currently Deposited.
Deposited can now include mempool outputs for static loop-ins, but
withdrawals and static channel opens still require confirmed funding
inputs. Filter automatic channel-open selection to confirmed deposits
and reject explicit unconfirmed selections, including withdraw-all
requests that would otherwise silently include mempool deposits.
Static address deposits with no confirmation height have not started
their CSV timeout yet, so keep them eligible for loop-in selection
instead of treating them as already near expiry. Prefer confirmed
deposits before unconfirmed ones during automatic selection, and share
the remaining-lifetime calculation used by the selector.
Use the shared deposit-expiry helper when building autoloop DP
candidates so unconfirmed deposits do not look like the
earliest-expiring options.

This keeps the no-change selector's expiry tie-break aligned with the
generic loop-in deposit selection rules.
Refresh the active static-address deposit set against lnd's wallet view
before quote, loop-in, withdrawal, channel-open, and autoloop selection
paths. This prevents stale persisted Deposited records from being
selected after replacement, reorg, or an external spend.
Use the deposit manager's visible-deposit view for normal list and
summary RPCs so historical Deposited rows whose outpoints vanished from
lnd's wallet view are not exposed as available funds.
Check the originally selected deposit outpoints before signing a static
loop-in HTLC transaction. If any selected outpoint is no longer
available, cancel the swap invoice and fail the signing action instead
of producing signatures for stale inputs.
Add cached per-swap notification fanout for static loop-in
confirmation-risk acceptance and rejection notifications so loop-in
FSMs can subscribe by swap hash and receive decisions that arrived
before subscription.
Subscribe to static loop-in confirmation-risk notifications before
starting the payment deadline. Start that deadline only after server
acceptance or the legacy confirmation fallback, and cancel the swap
invoice when the server rejects the risk wait. Refresh selected
deposits before the legacy fallback so recovered monitors use current
confirmation heights.
Add schema, sqlc queries, store fields, and SqlStore support for
recording server confirmation-risk decisions with static loop-in swaps.
Store the decision timestamp so payment-deadline recovery can
reconstruct elapsed time after restart.
Persist static loop-in confirmation-risk decisions before fanout when a
persistence callback is configured. Keep unpersisted decisions cached
for replay so notification delivery is not lost if the swap row is not
available yet.
Create the static loop-in SQL store before the notification manager and
pass a persistence callback so server confirmation-risk decisions are
durably recorded before fanout.
Track whether the invoice was canceled for non-payment while monitoring
the HTLC. If the HTLC never confirms before timeout, unlock the
deposits; if it did confirm, transition them to the HTLC-timeout sweep
state without issuing duplicate transitions.
Record replayed server risk decisions through the loop-in store,
recover accepted payment-deadline timers using the persisted decision
time, and handle persisted rejections on restart. This lets recovered
static loop-ins keep pending confirmation-risk state instead of
restarting payment timing from scratch.
Warn before dispatching a static loop-in that selects deposits below
the conservative six-confirmation threshold. Mirror automatic coin
selection before prompting so the warning reflects both manual and
auto-selected deposits.

Cover manual and auto-selected warning paths in CLI tests.
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. Import, persist,
and activate each script before returning it to callers.
Filter wallet UTXOs against every active static-address script and attach
the matching address parameters to new deposits. This makes deposits to
derived receive addresses visible to the deposit manager.
Build timeout sweeps from each deposit own script, expiry, and key
locator. Derived-address deposits can now use their unilateral recovery
path without falling back to the legacy root parameters.
Construct cooperative MuSig2 sessions from the address parameters stored
on each deposit. Loop-ins and withdrawals can therefore combine inputs
owned by different derived static addresses.
Carry each deposit client derivation key and an optional generated change
descriptor in static-address loop-in and withdrawal requests. The server
can use these fields to validate scripts it does not store directly.
Map every selected outpoint to the client key that derived its static
address and include those proofs in loop-in requests. Keep MuSig2 signing
indexed by outpoint so request ordering cannot select the wrong key.
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 each selected deposit with its persisted static-address row during
loop-in recovery. Hydrate legacy rows as needed so restored swaps retain
the scripts and key locators required for signing.
hieblmi added 5 commits July 14, 2026 11:49
Create a fresh static change address for fractional loop-ins and persist
its key locator with the selected HTLC outpoint. Recovery reconstructs the
same change output instead of returning funds to the legacy root address.
Create a fresh static address for partial-withdrawal change and include
its descriptor in the signing request. Build and validate the withdrawal
against that address while preserving full-withdrawal behavior.
Let loop static deposit create a fresh receive address and optionally fund
it through lnd SendCoins. Validate funding arguments before address
creation and expose the nested request through the client RPC.

Regenerate the RPC artifacts, CLI documentation, and replay fixtures for
the new command behavior.
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.

Update generated RPC artifacts and command replay fixtures for the new
field.
@hieblmi hieblmi self-assigned this Jul 14, 2026
@hieblmi hieblmi marked this pull request as draft July 14, 2026 12:58
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a foundational backup system for Loop's static-address state, enabling future HD-wallet-style recovery. It includes significant architectural improvements to deposit management, reconciliation, and database schema to support confirmation risk tracking and change address derivation. The changes also extend the CLI and RPC interfaces to provide better control over static address funding and deposit monitoring.

Highlights

  • Immutable Backup Service: Introduced a new backup service that creates encrypted, immutable backups for L402 tokens and static-address metadata, laying the groundwork for future HD-wallet-style recovery.
  • Database Schema Evolution: Applied multiple database migrations to support confirmation-risk decisions, track change addresses, and link deposits to specific static addresses.
  • Deposit Management Enhancements: Refined deposit reconciliation and management logic to ensure wallet views remain fresh and to handle confirmation risk decisions durably.
  • CLI and RPC Expansion: Added a new 'deposit' CLI command and updated RPC definitions to support funding static addresses and managing deposit-to-address metadata.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new backup service for Loop static-address and L402 state, adds a deposit command to the loop CLI, and updates the loopd daemon to handle static address loop-in risk decisions. The reviewer identified potential nil pointer dereference issues in NewStaticAddress and ListUnspentDeposits and provided code suggestions to implement defensive nil checks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 1706 to +1710
func (s *swapClientServer) NewStaticAddress(ctx context.Context,
_ *looprpc.NewStaticAddressRequest) (
req *looprpc.NewStaticAddressRequest) (
*looprpc.NewStaticAddressResponse, error) {

sendCoinsReq := req.GetSendCoinsRequest()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The staticAddressManager field can be nil in certain configurations or testing environments. To prevent a potential nil pointer dereference panic when calling NewAddress, add a defensive nil check at the beginning of NewStaticAddress.

Suggested change
func (s *swapClientServer) NewStaticAddress(ctx context.Context,
_ *looprpc.NewStaticAddressRequest) (
req *looprpc.NewStaticAddressRequest) (
*looprpc.NewStaticAddressResponse, error) {
sendCoinsReq := req.GetSendCoinsRequest()
func (s *swapClientServer) NewStaticAddress(ctx context.Context,
req *looprpc.NewStaticAddressRequest) (
*looprpc.NewStaticAddressResponse, error) {
if s.staticAddressManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "static address manager not configured")
}
sendCoinsReq := req.GetSendCoinsRequest()

Comment on lines 1882 to 1885
func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
req *looprpc.ListUnspentDepositsRequest) (
*looprpc.ListUnspentDepositsResponse, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The staticAddressManager field can be nil in certain configurations or testing environments. To prevent a potential nil pointer dereference panic when calling ListUnspentRaw, add a defensive nil check at the beginning of ListUnspentDeposits.

func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
	req *looprpc.ListUnspentDepositsRequest) (
	*looprpc.ListUnspentDepositsResponse, error) {

	if s.staticAddressManager == nil {
		return nil, status.Errorf(codes.FailedPrecondition, "static address manager not configured")
	}

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