Skip to content

lnwallet: fix InternalKeyForAddr for imported addr #9750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

starius
Copy link
Collaborator

@starius starius commented Apr 22, 2025

Change Description

If a Taproot address is added to LND using the ImportTapscript RPC, LND previously failed to perform a cooperative close to that address.

The issue was originally reported in the Loop repository: lightninglabs/loop#923

The root cause was in the lnwallet.InternalKeyForAddr function. This function assumed that all Taproot addresses implement the ManagedPubKeyAddress interface, implying that a private key is available. However, this assumption does not hold for externally imported addresses, which is why the function returned an error.

This PR updates the logic to add an exception for imported Taproot addresses.

In a separate commit, I also improved the error logging. Previously, the error message was missing the actual type:
unable to fetch internal key: expected pubkey addr, got <nil>
Now, the actual type is included, e.g.:
unable to fetch internal key: expected pubkey addr, got *waddrmgr.taprootScriptAddress

I added new test cases to the itest coop close with external delivery to reproduce the issue and verify the fix.

Additionally, I refactored the test to reuse Alice and Bob across cases. This reduced the number of blocks mined, also cutting test runtime by roughly two-thirds. This change was necessary because, after adding new cases, the block count exceeded the allowed limit of 50.

Steps to Test

$ make itest icase='coop close with external delivery'

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.

Copy link
Contributor

coderabbitai bot commented Apr 22, 2025

Important

Review skipped

Auto reviews are limited to specific labels.

🏷️ Labels to auto review (1)
  • llm-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@starius starius force-pushed the fix-InternalKeyForAddr-for-imported-addresses branch from d3fec7f to bbc834c Compare April 22, 2025 02:57
@starius starius marked this pull request as ready for review April 22, 2025 02:57
@yyforyongyu yyforyongyu requested a review from Copilot April 22, 2025 06:35
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a bug in InternalKeyForAddr by handling imported Taproot addresses and improves error logging by including the actual type in the error message. It also refactors the related cooperative close tests to share nodes for efficiency and updates the release notes accordingly.

  • Fix bug with Taproot addresses for imported keys.
  • Enhance error logging to display the correct address type.
  • Refactor and optimize cooperative close tests by reusing nodes.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
lnwallet/interface.go Update InternalKeyForAddr with exception for imported addresses.
itest/lnd_coop_close_external_delivery_test.go Refactor test cases to reuse nodes and adjust address handling.
docs/release-notes/release-notes-0.19.0.md Update release notes to include the fix for imported Taproot addresses.
Comments suppressed due to low confidence (1)

itest/lnd_coop_close_external_delivery_test.go:19

  • [nitpick] Consider using consistent capitalization for node names (e.g., 'Bob' instead of 'bob') to improve overall readability and consistency with 'Alice'.
bob := ht.NewNodeWithCoins("bob", nil)

Copy link
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Very nice commit structure! Left some questions and comments.

Previously when handling external taproot addresses, we would exit here due to the address cannot be found as lnd is not aware of the internal key.

lnd/lnwallet/interface.go

Lines 639 to 654 in c9fe051

walletAddr, err := wallet.AddressInfo(addr)
if err != nil {
// If the error is that the address can't be found, it is not
// an error. This happens when any channel which is not a custom
// taproot channel is cooperatively closed to an external P2TR
// address. In this case there is no internal key associated
// with the address. Callers can use the .Option() method to get
// an option value.
var managerErr waddrmgr.ManagerError
if errors.As(err, &managerErr) &&
managerErr.ErrorCode == waddrmgr.ErrAddressNotFound {
return none, nil
}
return none, err

However when these addresses are imported, it will pass this check and lead to the failure in the itest. I think we might as well just return early if we know a given address is imported.

alice := ht.NewNodeWithCoins("Alice", nil)
bob := ht.NewNodeWithCoins("bob", nil)
ht.ConnectNodes(alice, bob)
alice, bob *node.HarnessNode, upfrontShutdown bool,
Copy link
Member

Choose a reason for hiding this comment

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

I think it's better to start new nodes to keep each test independent. We can actually do sth similar to

var walletImportAccountTestCases = []*lntest.TestCase{

So sth like below, which keeps blocks mined small, also nice for the itest scheduler to load balance.

var coopCloseWithExternalTestCases = []*lntest.TestCase{
	{
		Name: "set P2WPKH delivery address at open",
		TestFunc: func(ht *lntest.HarnessTest) {
			testCoopCloseWithExternalDeliveryImpl(
				ht, true, false,
				lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH,
			)
		},
	},
	{
		Name: "set P2WPKH delivery address at close",
		TestFunc: func(t *lntest.HarnessTest){
			...
		},
	},
        ...
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! I used this test structure in the reworked itest commits.

FullKeyOnly: true,
},
}
res := alice.RPC.ImportTapscript(req)
Copy link
Member

Choose a reason for hiding this comment

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

Do keys imported via ImportPublicKey also have this issue? I think not since the internal keys are unknown to lnd?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added more test cases for "coop close with external delivery" to safeguard against this in the future:

  • P2TR address imported with ImportPublicKey
  • P2WPKH address imported with ImportPublicKey

// do not implement waddrmgr.ManagedPubKeyAddress.
// See RPC ImportTapscript.
// Fix https://github.com/lightninglabs/loop/issues/923
if walletAddr.Imported() {
Copy link
Member

Choose a reason for hiding this comment

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

If we are sure that we don't have the internal key for any imported wallet addresses, we might as well just check walletAddr.Imported() before the interface check, or even earlier.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I think we can move this check to reside before the type assertion (but after the nil check).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I moved the check before the type assertion but after the nil check.

@guggero guggero self-requested a review April 22, 2025 07:30
@starius starius force-pushed the fix-InternalKeyForAddr-for-imported-addresses branch from bbc834c to 1485fae Compare April 22, 2025 15:14
@starius starius requested a review from yyforyongyu April 22, 2025 15:18
@saubyk saubyk added this to the v0.19.0 milestone Apr 22, 2025
@starius starius force-pushed the fix-InternalKeyForAddr-for-imported-addresses branch from 4f5d898 to e4b000b Compare April 22, 2025 17:44
Copy link
Member

@Roasbeef Roasbeef 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 this fix!

The change looks good, and great to expand the set of itests here to address this gap.

// do not implement waddrmgr.ManagedPubKeyAddress.
// See RPC ImportTapscript.
// Fix https://github.com/lightninglabs/loop/issues/923
if walletAddr.Imported() {
Copy link
Member

Choose a reason for hiding this comment

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

Yeah I think we can move this check to reside before the type assertion (but after the nil check).

pk := [33]byte{2, 3, 4}
if upfrontShutdown {
// Make new address for second sub-test.
pk[1]++
Copy link
Member

Choose a reason for hiding this comment

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

Is this actually doing anything? There's no re-assignment here.

Copy link
Member

Choose a reason for hiding this comment

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

Ah TIL, kind of surprising behavior: https://go.dev/play/p/W07kpFn0B7M

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, this operation changes the value in the array in-place.

@starius starius force-pushed the fix-InternalKeyForAddr-for-imported-addresses branch from e4b000b to 1266c57 Compare April 22, 2025 23:13
Copy link
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

LGTM🦾 Only a few nits.

tt, false, lnrpc.AddressType_UNUSED_TAPROOT_PUBKEY,
)
})
var coopCloseWithExternalTestCases = []*lntest.TestCase{
Copy link
Member

Choose a reason for hiding this comment

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

👍

@@ -76,7 +75,10 @@ var excludedTestsWindows = []string{
// more investigation is needed.
"channel force close-anchor restart",
"channel force close-simple taproot restart",
}
}, extractNames(
Copy link
Member

Choose a reason for hiding this comment

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

hmmm, how about we add this to L103? Seems easier this way, also keep this excludedTestsWindows simple

                // The name for a bubtest case has the format
		// "subtestName-caseName", to exclude a whole subtest, we match
		// against "subtestName".
		name := strings.Split(tc.Name, "-")[0]
		if excludedSet.Contains(name) {
			excludedSet.Remove(name)

			continue
		}

Copy link
Collaborator

Choose a reason for hiding this comment

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

This might give us false positives with names like channel force close-anchor restart...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"channel force close" is also a prefixed test.

In itest/list_on_test.go:

        allTestCases = appendPrefixed(
                "channel force close", allTestCases, channelForceCloseTestCases,
        )

Removing "channel force close" from excludedSet should not be an issue, since it is not present there. It would be a no-op.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I actually prefer the original approach (extractNames). It is explicit and clear that this is a group of prefixed tests.

},
},
{
Name: "set imported P2TR address (ImportTapscript) at open",
Copy link
Member

Choose a reason for hiding this comment

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

💯

@@ -53,20 +109,102 @@ var coopCloseWithExternalTestCases = []*lntest.TestCase{
// balance irrespective of whether the delivery address is in LND's wallet or
// not. Some users set this value to be an address in a different wallet and
// this should not affect our ability to accurately report the settled balance.
//
// If importTapscript is set, it imports a Taproot script and internal key to
// alice LND using ImportTapscript to make sure it doesn't interfere with
Copy link
Member

Choose a reason for hiding this comment

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

nit: Alice's LND

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Collaborator

@guggero guggero left a comment

Choose a reason for hiding this comment

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

Very nice, LGTM 🎉

@@ -76,7 +75,10 @@ var excludedTestsWindows = []string{
// more investigation is needed.
"channel force close-anchor restart",
"channel force close-simple taproot restart",
}
}, extractNames(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This might give us false positives with names like channel force close-anchor restart...

starius added 2 commits April 23, 2025 11:17
Include the variable of interest (walletAddr), not the outcome of the check
(pubKeyAddr) which is always nil.
An address imported using ImportTapscript doesn't provide a private key
so it can't satisfy waddrmgr.ManagedPubKeyAddress interface. So we don't
return an error for imported addresses.
Fix lightninglabs/loop#923
@starius starius force-pushed the fix-InternalKeyForAddr-for-imported-addresses branch from 1266c57 to 4aac7d3 Compare April 23, 2025 14:38
@starius starius requested review from guggero and yyforyongyu April 23, 2025 14:40
starius added 3 commits April 23, 2025 11:52
More sub-tests are needed in "coop close with external delivery" itest, which
would break the limit for 50 blocks mined. Turning it into prefixed itest.

Also used new exclusion approach in test "remote signer" where all the prefixed
tests are excluded.
Make sure that an address imported to LND via ImportTapscript or ImportPublicKey
can be used as a delivery address in coop close.

New test cases:
 - P2TR address imported with ImportTapscript
 - P2TR address imported with ImportPublicKey
 - P2WPKH address imported with ImportPublicKey

Safeguard against lightninglabs/loop#923
@starius starius force-pushed the fix-InternalKeyForAddr-for-imported-addresses branch from 4aac7d3 to dc2cad9 Compare April 23, 2025 14:57
@starius
Copy link
Collaborator Author

starius commented Apr 23, 2025

@guggero @yyforyongyu I also used extractNames for "remote signer" tests. Sorry for force pushing!

@guggero guggero merged commit 4862251 into lightningnetwork:master Apr 23, 2025
30 of 31 checks passed
@starius starius deleted the fix-InternalKeyForAddr-for-imported-addresses branch April 23, 2025 16:30
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.

5 participants