Skip to content

recovery: L402 and static address recovery from local backup file#1121

Draft
hieblmi wants to merge 6 commits intolightninglabs:masterfrom
hieblmi:recover-l402-static
Draft

recovery: L402 and static address recovery from local backup file#1121
hieblmi wants to merge 6 commits intolightninglabs:masterfrom
hieblmi:recover-l402-static

Conversation

@hieblmi
Copy link
Copy Markdown
Collaborator

@hieblmi hieblmi commented Apr 14, 2026

This PR adds phase-1 local recovery for static-address and L402 client state.

When loopd starts and finds an existing static address or local l402.token* state, it now writes an encrypted backup file into the active Loop data
directory. A fresh client can later restore from that file with loop recover --backup_file , or by omitting the flag and using the default
backup path.

What Changed

  • Added a new recovery package that owns backup/restore orchestration, file format, encryption, and documentation.
  • Added startup backup creation in loopd.
  • Added a recovery RPC endpoint in loopd.
  • Added loop recover to the CLI.
  • Added static-address manager restore support so restore can reuse existing address import/store logic.
  • Added a deposit-manager reconciliation entrypoint so restore can trigger best-effort deposit discovery after state import.
  • Added package documentation in recovery/README.md.

Backup Contents

The encrypted backup stores:

  • Backup version
  • Active network
  • Static-address protocol version
  • Static-address server pubkey
  • Static-address expiry
  • Exact client key locator
  • Static-address pkScript
  • Derived taproot address string
  • Initiation height
  • Raw local l402.token* file contents

The L402 state is backed up as raw file blobs rather than decomposed token fields so restore stays compatible with Aperture’s current file-store
format.

Restore Flow

loop recover now restores the local client state by:

  1. Reading and decrypting the backup file
  2. Validating backup version and network
  3. Re-deriving the static-address client key from the backed-up locator
  4. Falling back to a gap-20 scan in the static-address key family if the locator is missing or unusable
  5. Re-creating the static-address DB record
  6. Re-importing the static-address tapscript into lnd
  7. Restoring the raw L402 token files into the local token store
  8. Running best-effort deposit reconciliation

Encryption

The backup file is encrypted with a deterministic key derived from lnd using Signer.DeriveSharedKey, so the backup remains bound to the same
underlying seed material without requiring an interactive password in phase 1.

Implementation Notes

  • The recovery logic was intentionally extracted into a standalone recovery package to keep transport and daemon wiring thin.
  • The restore endpoint is currently exposed as a manual gRPC service used by loop recover.
  • This PR does not attempt a full historical on-chain rebuild of deposit state. It restores the static-address state and then relies on the existing
    deposit reconciliation flow for best-effort recovery.

@hieblmi hieblmi marked this pull request as draft April 14, 2026 10:09
@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 significantly enhances the resilience of Loop by introducing a robust local recovery system. It allows users to restore their static address and L402 client state from an encrypted backup, ensuring continuity of operations even after data loss or a fresh installation. The changes include automated backup creation on startup, a new CLI command for restoration, and a dedicated recovery service that handles encryption, key derivation, and integration with existing static address and deposit management functionalities.

Highlights

  • Local Recovery for Static Address and L402 State: Introduced a phase-1 local recovery mechanism for Loop's static address and L402 client state, allowing restoration from an encrypted backup file.
  • Automated Backup Creation: Implemented automatic creation of an encrypted backup file in the active Loop data directory when loopd starts and finds existing static address or L402 token state.
  • New Recovery Package and CLI Command: Added a dedicated recovery package to orchestrate backup/restore operations, handle file formats and encryption, and introduced a loop recover CLI command for manual restoration.
  • Integrated Restoration Process: Enabled the static-address manager to support restoration by reusing existing address import logic and integrated a deposit-manager reconciliation entrypoint for best-effort deposit discovery after state import.

🧠 New Feature in Public Preview: You can now enable Memory 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 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 counter productive. 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.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

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 local recovery mechanism for Loop static addresses and L402 authentication state. It includes a new recovery package for managing encrypted backups, a recover CLI command, and a gRPC service to trigger the restoration process. On the daemon side, backups are automatically generated during startup. Feedback suggests that backup failures during startup should not prevent the daemon from running and recommends enhancing the atomic file writing logic with explicit synchronization and better temporary file cleanup.

loopd/daemon.go Outdated
Comment on lines +698 to +704
backupFile, err := recoveryService.WriteBackup(d.mainCtx)
if err != nil {
return fmt.Errorf("unable to write backup file: %w", err)
}
if backupFile != "" {
infof("Wrote encrypted backup file to %s", backupFile)
}
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

Failing to write a backup file on startup should probably not prevent the entire daemon from starting. While backups are important, a failure here (e.g., due to temporary disk issues or permission problems) shouldn't cause a regression in the availability of the swap service. Consider logging the error and continuing instead of returning it.

	recoveryService := recovery.NewService(
		d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
		staticAddressManager, depositManager,
	)
	backupFile, err := recoveryService.WriteBackup(d.mainCtx)
	if err != nil {
		errorf("Unable to write backup file: %v", err)
	} else if backupFile != "" {
		infof("Wrote encrypted backup file to %s", backupFile)
	}

Comment on lines +574 to +583
func writeFileAtomically(path string, data []byte, mode os.FileMode) error {
tempPath := path + ".tmp"

err := os.WriteFile(tempPath, data, mode)
if err != nil {
return err
}

return os.Rename(tempPath, path)
}
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

To ensure the durability of the backup file, it is recommended to Sync() the file before closing and renaming it. Additionally, using defer os.Remove(tempPath) ensures that the temporary file is cleaned up if the rename operation fails.

func writeFileAtomically(path string, data []byte, mode os.FileMode) error {
	tempPath := path + ".tmp"

	f, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
	if err != nil {
		return err
	}
	defer func() {
		f.Close()
		_ = os.Remove(tempPath)
	}()

	if _, err := f.Write(data); err != nil {
		return err
	}

	if err := f.Sync(); err != nil {
		return err
	}

	if err := f.Close(); err != nil {
		return err
	}

	return os.Rename(tempPath, path)
}

@hieblmi hieblmi force-pushed the recover-l402-static branch from bcf3b89 to 17804b9 Compare April 14, 2026 14:09
@hieblmi hieblmi force-pushed the recover-l402-static branch from 17804b9 to a2e5f02 Compare April 14, 2026 14:28
hieblmi added 5 commits April 14, 2026 16:52
Introduce a dedicated recovery package for encrypted local backups of
static-address state and raw l402 token files.

The package owns the backup file format, seed-derived encryption,
static-address key re-derivation with gap fallback, token file restore,
and best-effort deposit reconciliation orchestration. It also includes
package-level documentation and focused tests for the file helpers.
Grant the manual recovery RPC the auth-write and static-address loop-in
permissions it needs so loopd can restore local L402 material and
static-address state through the authenticated client connection.
Register a recovery gRPC endpoint, attach the new recovery package to the
swap client server, and have daemon startup write an encrypted backup file
whenever recoverable static-address or l402 state already exists locally.
Expose the new recovery flow through loop-cli with a dedicated recover
command that calls loopd over the existing RPC connection and optionally
accepts a custom backup file path.
@hieblmi hieblmi force-pushed the recover-l402-static branch from a2e5f02 to 83e2547 Compare April 14, 2026 14:53
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