Skip to content

config_builder: preserve watch-only account numbers#10644

Open
starius wants to merge 4 commits intolightningnetwork:masterfrom
starius:preserve-accounts
Open

config_builder: preserve watch-only account numbers#10644
starius wants to merge 4 commits intolightningnetwork:masterfrom
starius:preserve-accounts

Conversation

@starius
Copy link
Copy Markdown
Collaborator

@starius starius commented Mar 16, 2026

Change Description

Import non-zero watch-only accounts at explicit account numbers instead of allocating them contiguously. Allow using an arbitrary account number and to have space account number space on watch-only.

Steps to Test

Unit test:

go test -run TestImportWatchOnlyAccountsPreservesSparseAccountNumbers

itest:

make itest icase='remote_signer-sparse_account_import' backend=btcd timeout=90m

Manual test:

  1. On signer (full-seed node), force-create/use sparse family 43210:
  grpcurl -cacert ~/.lnd/tls.cert \
    -H "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 ~/.lnd/data/chain/bitcoin/regtest/admin.macaroon)" \
    -d '{"key_family":43210,"key_index":0}' \
    localhost:10009 walletrpc.WalletKit/DeriveKey
  1. Export accounts JSON from signer:
  lncli wallet accounts list > accounts-signer.json
  grep "m/1017'/1'/43210'" accounts-signer.json
  1. On watch-only node (fresh datadir, remote signer enabled), import:
  lncli createwatchonly accounts-signer.json
  1. On watch-only, derive the same key:
  grpcurl -cacert ~/.lnd/tls.cert \
    -H "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 ~/.lnd/data/chain/bitcoin/regtest/admin.macaroon)" \
    -d '{"key_family":43210,"key_index":0}' \
    localhost:10009 walletrpc.WalletKit/DeriveKey
  1. Compare signer vs watch-only raw_key_bytes outputs. What to watch:
  • No error like account 43210 not found.
  • Same derived pubkey on signer and watch-only for (43210, 0).
  • Optional: lncli wallet accounts list on watch-only shows account path m/1017'/1'/43210' (not remapped to account 256).

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

@starius starius marked this pull request as draft March 16, 2026 07:37
@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 enhances the lnd wallet's watch-only functionality, specifically for remote signer configurations. It modifies the account import process to ensure that explicit, non-zero account numbers are retained rather than being remapped contiguously. This change is critical for remote signer setups that utilize sparse custom key families, as it guarantees that key derivation requests for these accounts will correctly resolve to their intended indices, thereby improving the robustness and flexibility of remote signing operations.

Highlights

  • Sparse Account Preservation: Watch-only wallet account imports now preserve explicit non-zero account numbers per scope, addressing issues in remote-signer setups where DeriveKey(KeyFamily=N) failed for sparse custom key families.
  • Remote Signer Compatibility: This change allows remote signers to use arbitrary account numbers and maintain sparse account number space for watch-only wallets, improving flexibility and accuracy in key derivation.
  • New Unit and Integration Tests: Dedicated unit and integration tests were added to ensure the correct preservation of sparse account numbers during watch-only wallet initialization.

🧠 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.

Changelog
  • config_builder.go
    • Imported "encoding/binary", "hdkeychain", and "netparams" packages.
    • Defined "waddrmgrNamespaceKey" for wallet manager state.
    • Refactored "importWatchOnlyAccounts" to delegate to a new "importWatchOnlyAccount" function.
    • Added "importWatchOnlyAccount" to handle explicit account number imports for non-zero indices, preserving sparse account numbers.
    • Implemented "validateWatchOnlyAccountPubKey" and "isPubKeyForNet" for key validation mirroring btcwallet's internal logic.
  • config_builder_watchonly_test.go
    • Added a new test file.
    • Implemented "TestImportWatchOnlyAccountsPreservesSparseAccountNumbers" to verify that sparse account numbers are preserved during watch-only wallet initialization.
    • Included "deriveAccountXpub" helper function for test setup.
  • docs/release-notes/release-notes-0.21.0.md
    • Added a release note detailing that watch-only wallet account imports now preserve explicit non-zero account numbers.
  • docs/remote-signing.md
    • Updated the documentation to clarify that lnd now preserves explicit account numbers for custom m/1017'/X'/N' accounts in watch-only wallets.
  • itest/lnd_remote_signer_test.go
    • Added "testRemoteSignerSparseAccountImport" to the "remoteSignerTestCases" slice.
    • Modified "remoteSignerTestCase" struct to include an "accounts" field.
    • Updated "prepareRemoteSignerTest" to use the new "accounts" field for watch-only account initialization.
    • Implemented "testRemoteSignerSparseAccountImport" to verify key derivation for sparse custom key families in a remote signer setup.
    • Modified "deriveCustomScopeAccounts" to accept additional sparse account indices.
Activity
  • The pull request is currently marked as "[WIP]", indicating ongoing development or an early stage of review.
  • The author has provided detailed steps for unit, integration, and manual testing.
  • Several items in the "Pull Request Checklist" related to testing and code style are unchecked, suggesting further work or review is needed.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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 significant improvement for watch-only wallets, particularly in remote signer setups, by preserving explicit account numbers during import. The changes are well-implemented, with clear refactoring in config_builder.go and comprehensive new unit and integration tests. The documentation has also been updated accordingly. I have one minor suggestion regarding code style to improve consistency with the repository's guidelines.

Copy link
Copy Markdown
Collaborator

@ViktorT-11 ViktorT-11 left a comment

Choose a reason for hiding this comment

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

Thanks for discovering this! Approach ACK 🔥🙏! Leaving a few suggestions below, but non of them are necessarily blocking from my end.

Comment on lines +1696 to +1703
// validateWatchOnlyAccountPubKey ensures that a watch-only account xpub has
// the same structure and network restrictions as btcwallet's
// ImportAccountWithScope path.
//
// NOTE: This intentionally mirrors btcwallet's internal validation in
// wallet/import.go: (*Wallet).validateExtendedPubKey(..., isAccountKey=true).
// Keep both implementations aligned.
func validateWatchOnlyAccountPubKey(wallet *wallet.Wallet,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I guess having to duplicate this & isPubKeyForNet from btcwallet is the only thing I don't really like with this approach as it'll be hard to ensure that the implementations remain aligned between the different repos. It would be much better if you could use some public API that ensures both repos use the same validation logic.

As a potentially suggestion that already exists today, you could look into if you could use wallet.ImportAccountDryRun for the non if scope.Index == 0 clause, and check if that errors or not.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I re-used wallet.ImportAccountDryRun for validation and got rid of validateWatchOnlyAccountPubKey and isPubKeyForNet.

Comment on lines +1648 to +1665
scopedMgr, err := wallet.Manager.FetchScopedKeyManager(
scope.Scope,
)
if err != nil {
var manErr waddrmgr.ManagerError
if !errors.As(err, &manErr) ||
manErr.ErrorCode != waddrmgr.ErrScopeNotFound {

return err
}

scopedMgr, err = wallet.Manager.NewScopedKeyManager(
addrmgrNs, scope.Scope, addrSchema,
)
if err != nil {
return err
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think there's value in adding some docs for explaining the logic here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added a comment:

// Mirror what ImportAccountWithScope in btcwallet itself does:
// try to find an already-registered manager for this key scope.
// If the scope was already set up, we reuse it. Otherwise we
// create the scope in the DB and register the in-memory
// manager.

- Any additional custom `m/1017'/X'/N'` accounts that your signer actively
uses (for example `N=43210`). Newer `lnd` versions preserve these explicit
account numbers in watch-only wallets, so `DeriveKey(KeyFamily=N)` works
without requiring contiguous account creation.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do you believe there'd be value in mentioning the implications of downgrading a watch-only node to an lnd version which hasn't implemented this new behaviour, if this new behaviour has been utilised? Potentially that'd belong to the release notes though.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added it both here and in the release notes. Downgrading should be safe in terms that the keys already imported in 0.21 should remain properly mapped after downgrading to 0.20, since they are already stored in proper places in the wallet DB. However, if after downgrading another key is imported, it will be added at next available slot, breaking the mapping, but this is separate from downgrading (this is just what the current behavior is).

starius added 4 commits April 1, 2026 13:05
Import non-zero watch-only accounts at explicit account numbers instead of
allocating them contiguously.

Keep account 0 on the existing ImportAccountWithScope path to preserve
the "default" account semantics and naming that the rest of lnd expects.
Non-zero accounts use NewRawAccountWatchingOnly so the btcwallet account
number matches the key family index, which is critical for remote-signer
setups where DeriveKey(KeyFamily=N) must resolve to account N.

Validate non-zero account xpubs via wallet.ImportAccountDryRun, reusing
btcwallet's own validation (validateExtendedPubKey / isPubKeyForNet)
rather than duplicating its internals. The dry-run rolls back its
database transaction, so no persistent state is created.

The previous code already used one walletdb transaction per account
(each ImportAccountWithScope call opens its own). This commit preserves
that existing behaviour rather than introducing a single wrapping
transaction. The inline error comment previously claimed all accounts
ran in the "same transaction"; that was already inaccurate before this
change and has been corrected.

Add a regression test that verifies sparse key families (e.g. 43210)
can be derived after watch-only import.
Add a dedicated remote signer integration test for sparse custom key
families. The test initializes the watch-only node with account 43210 in
the 1017 scope and asserts DeriveKey returns the same key on signer and
watch-only nodes.

Also extend deriveCustomScopeAccounts to accept extra account indices so
the sparse case can be constructed explicitly.
Clarify that watch-only remote-signer wallets should include custom
1017/X/N accounts used by the signer.

Document that lnd v0.21.0+ preserves explicit account numbers so
DeriveKey(KeyFamily=N) works for sparse families. Note that older
versions import accounts contiguously, but downgrading after wallet
creation is safe because accounts are already persisted at the correct
indices and are not re-imported on unlock.
Add a bug-fix entry for preserving explicit watch-only account numbers in
remote-signer mode. Mention the sparse key-family derivation case where accounts
were previously remapped contiguously. Note that account numbers are written at
wallet creation time and not re-imported on unlock, so downgrading does not
remap them.
@starius starius force-pushed the preserve-accounts branch from a4d62d9 to e8f2cdc Compare April 1, 2026 18:06
@starius starius changed the title [WIP] config_builder: preserve watch-only account numbers config_builder: preserve watch-only account numbers Apr 1, 2026
@starius starius marked this pull request as ready for review April 1, 2026 18:15
@starius starius requested a review from ViktorT-11 April 1, 2026 18:15
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.

2 participants