Skip to content

lndclient: attach key locator when any component is non-zero#269

Merged
Roasbeef merged 2 commits intomasterfrom
signoutput-fix
May 5, 2026
Merged

lndclient: attach key locator when any component is non-zero#269
Roasbeef merged 2 commits intomasterfrom
signoutput-fix

Conversation

@Roasbeef
Copy link
Copy Markdown
Member

@Roasbeef Roasbeef commented Apr 18, 2026

In this PR, we fix a latent bug in marshallSignDescriptors's fullDescriptor inner closure (the one used by SignOutputRawKeyLocator). The helper currently attaches the RPC KeyLoc only when both KeyLocator.Family != 0 and KeyLocator.Index != 0. That && is wrong: it silently drops the locator for any valid key whose slot has a zero component, and callers that need the locator to re-derive the key (i.e., they can't just scan for a matching pubkey) end up with a descriptor that no longer identifies the key.

The most visible case is LND's node identity key at keychain.KeyFamilyNodeKey (family 6) / index 0. Family 6 is meaningful, index 0 is meaningful (it's the first key), so a caller that populates both ends up with a descriptor that forwards neither. The same goes for any first-index key in a non-zero family, which is exactly what a restored-from-seed wallet has before anything else is derived. We hit this downstream in darepo-client and had to carry a full local copy of SignOutputRaw just to relax this one predicate. Once this lands the local copy can go away.

The fix flips && to ||: attach the locator whenever either the family or the index is non-zero, and only omit it for the all-zero KeyLocator (which is the struct's zero value and therefore indistinguishable from "unset" at call sites). We also rewrite the surrounding comment block so the new rule and its rationale are clear at the call site.

The legacy partial descriptor path used by SignOutputRaw is intentionally untouched. Loop and other downstream callers have adjusted themselves to its "populate either the pubkey or the locator, not both" shape, which is the whole reason SignOutputRawKeyLocator exists as a separate method in the first place.

Test Plan

A new signer_client_test.go pins the contract with table-driven tests:

  • TestMarshallSignDescriptorsFullLocator walks the four (family, index) quadrants against the fullDescriptor path. The family-only_locator_is_attached subtest (family 6, index 0) is the specific regression guard and fails against the old && predicate.
  • TestMarshallSignDescriptorsPartialOnlyOneOf covers the legacy partialDescriptor path to pin that it still populates either the pubkey or the locator but never both, so we don't regress Loop or similar downstream callers.
$ go test -run '^TestMarshallSignDescriptors' ./...
ok  	github.com/lightninglabs/lndclient	0.334s

SignOutputRawKeyLocator was introduced to propagate the key locator
alongside the raw public key, but its marshaller required both the
family and the index to be non-zero before attaching the KeyLoc.
That silently dropped the locator for valid keys whose slot has a
zero component — most visibly LND's node identity key at family 6 /
index 0, but any first-index key in a non-zero family is affected.

Attach the locator whenever either component is non-zero, and keep
omitting it only for the all-zero KeyLocator, which is the struct's
zero value and therefore indistinguishable from "locator not set"
at call sites.

This lets callers that sign against the node identity key path, or
any first-index key in a restored-from-seed wallet, resolve through
the fully populated descriptor instead of falling back to pubkey
scanning.
Copy link
Copy Markdown
Contributor

@starius starius left a comment

Choose a reason for hiding this comment

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

LGTM! 💾

Great catch! I propose to add a couple of test cases and to remove the redundant tc := tc (lint finding).

Comment thread signer_client_test.go
func TestMarshallSignDescriptorsFullLocator(t *testing.T) {
t.Parallel()

pubKey := newTestPubKey(t)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I propose to add two more tests where KeyDesc.PubKey is nil and either family!=0 or index!=0. I.e. replicate "family-only locator is attached" and "index-only locator is attached" for the KeyDesc.PubKey=nil case.

Copy link
Copy Markdown

@ziggie1984 ziggie1984 left a comment

Choose a reason for hiding this comment

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

LGTM, this should not effect any implementations using lndclient with LND.

Pin the contract that SignOutputRawKeyLocator's marshaller attaches
the key locator whenever either the family or the index is non-zero
and omits it only for the all-zero KeyLocator. The family-only case
(KeyFamilyNodeKey / index 0) is the specific regression guard for
the previously "both components non-zero" predicate that silently
dropped the locator for LND's node identity key slot.

Also cover the legacy partial descriptor path used by SignOutputRaw
to pin that it still populates either the public key or the locator
but never both, matching the behavior Loop and other downstream
callers adjusted themselves to.
@Roasbeef Roasbeef merged commit a008faf into master May 5, 2026
1 check passed
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.

3 participants