Skip to content

Commit afb4b2d

Browse files
committed
signer: create workaround for SignOutputRaw quirk
This commit fixes a long-standing issue with how SignOutputRaw populates the key descriptor before calling into lnd. If the public key is available, _only_ the public key is populated and the key locator (index+family) is not. That works well for any keys the wallet is aware of. But if a wallet is restored from seed, it will not know any addresses/keys apart from index 0 of each family/account. And a lookup by public key only will fail. To fix that, we add a new method SignOutputRawKeyLocator that has an updated behavior that also sends along the key locator if we're certain it is fully known. Because changing any behavior in this area of the code might lead to breaking existing behavior some clients like Loop or Pool rely on, we explicitly don't change the original method but rather add a new one.
1 parent 63e1f38 commit afb4b2d

File tree

2 files changed

+113
-40
lines changed

2 files changed

+113
-40
lines changed

macaroon_recipes.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,24 @@ var (
2727
// implemented in lndclient and the value is the original name of the
2828
// RPC method defined in the proto.
2929
renames = map[string]string{
30-
"ChannelBackup": "ExportChannelBackup",
31-
"ChannelBackups": "ExportAllChannelBackups",
32-
"ConfirmedWalletBalance": "WalletBalance",
33-
"Connect": "ConnectPeer",
34-
"DecodePaymentRequest": "DecodePayReq",
35-
"ListTransactions": "GetTransactions",
36-
"PayInvoice": "SendPaymentSync",
37-
"UpdateChanPolicy": "UpdateChannelPolicy",
38-
"NetworkInfo": "GetNetworkInfo",
39-
"SubscribeGraph": "SubscribeChannelGraph",
40-
"InterceptHtlcs": "HtlcInterceptor",
41-
"ImportMissionControl": "XImportMissionControl",
42-
"EstimateFeeRate": "EstimateFee",
43-
"EstimateFeeToP2WSH": "EstimateFee",
44-
"OpenChannelStream": "OpenChannel",
45-
"ListSweepsVerbose": "ListSweeps",
46-
"MinRelayFee": "EstimateFee",
30+
"ChannelBackup": "ExportChannelBackup",
31+
"ChannelBackups": "ExportAllChannelBackups",
32+
"ConfirmedWalletBalance": "WalletBalance",
33+
"Connect": "ConnectPeer",
34+
"DecodePaymentRequest": "DecodePayReq",
35+
"ListTransactions": "GetTransactions",
36+
"PayInvoice": "SendPaymentSync",
37+
"UpdateChanPolicy": "UpdateChannelPolicy",
38+
"NetworkInfo": "GetNetworkInfo",
39+
"SubscribeGraph": "SubscribeChannelGraph",
40+
"InterceptHtlcs": "HtlcInterceptor",
41+
"ImportMissionControl": "XImportMissionControl",
42+
"EstimateFeeRate": "EstimateFee",
43+
"EstimateFeeToP2WSH": "EstimateFee",
44+
"OpenChannelStream": "OpenChannel",
45+
"ListSweepsVerbose": "ListSweeps",
46+
"MinRelayFee": "EstimateFee",
47+
"SignOutputRawKeyLocator": "SignOutputRaw",
4748
}
4849

4950
// ignores is a list of method names on the client implementations that

signer_client.go

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ type SignerClient interface {
2828
signDescriptors []*SignDescriptor,
2929
prevOutputs []*wire.TxOut) ([][]byte, error)
3030

31+
// SignOutputRawKeyLocator is a copy of the SignOutputRaw that fixes a
32+
// specific issue around how the key locator is populated in the sign
33+
// descriptor. We copy this method instead of fixing the original to
34+
// make sure we don't break any existing applications that have already
35+
// adjusted themselves to use the specific behavior of the original
36+
// SignOutputRaw method.
37+
SignOutputRawKeyLocator(ctx context.Context, tx *wire.MsgTx,
38+
signDescriptors []*SignDescriptor,
39+
prevOutputs []*wire.TxOut) ([][]byte, error)
40+
3141
// ComputeInputScript generates the proper input script for P2WPKH
3242
// output and NP2WPKH outputs. This method only requires that the
3343
// `Output`, `HashType`, `SigHashes` and `InputIndex` fields are
@@ -215,26 +225,70 @@ func (s *signerClient) RawClientWithMacAuth(
215225
return s.signerMac.WithMacaroonAuth(parentCtx), s.timeout, s.client
216226
}
217227

218-
func marshallSignDescriptors(
219-
signDescriptors []*SignDescriptor) []*signrpc.SignDescriptor {
228+
func marshallSignDescriptors(signDescriptors []*SignDescriptor,
229+
fullDescriptors bool) []*signrpc.SignDescriptor {
220230

221-
rpcSignDescs := make([]*signrpc.SignDescriptor, len(signDescriptors))
222-
for i, signDesc := range signDescriptors {
223-
var keyBytes []byte
224-
var keyLocator *signrpc.KeyLocator
225-
if signDesc.KeyDesc.PubKey != nil {
226-
keyBytes = signDesc.KeyDesc.PubKey.SerializeCompressed()
231+
// partialDescriptor is a helper method that creates a partially
232+
// populated sign descriptor that is backward compatible with the way
233+
// some applications like Loop expect the call to lnd to be made. This
234+
// function only populates _either_ the public key or the key locator in
235+
// the descriptor, but not both.
236+
partialDescriptor := func(
237+
d keychain.KeyDescriptor) *signrpc.KeyDescriptor {
238+
239+
keyDesc := &signrpc.KeyDescriptor{}
240+
if d.PubKey != nil {
241+
keyDesc.RawKeyBytes = d.PubKey.SerializeCompressed()
227242
} else {
228-
keyLocator = &signrpc.KeyLocator{
229-
KeyFamily: int32(
230-
signDesc.KeyDesc.KeyLocator.Family,
231-
),
232-
KeyIndex: int32(
233-
signDesc.KeyDesc.KeyLocator.Index,
234-
),
243+
keyDesc.KeyLoc = &signrpc.KeyLocator{
244+
KeyFamily: int32(d.KeyLocator.Family),
245+
KeyIndex: int32(d.KeyLocator.Index),
235246
}
236247
}
237248

249+
return keyDesc
250+
}
251+
252+
// fullDescriptor is a helper method that creates a fully populated sign
253+
// descriptor that includes both the public key and the key locator (if
254+
// available). For the locator we explicitly check that both the family
255+
// _and_ the index is non-zero. In some applications it's possible that
256+
// the family is always set (because only a specific family is used),
257+
// but the index might be zero because it's the first key, or because it
258+
// isn't known at that particular moment.
259+
// We aim to be compatible with this method in lnd's wallet:
260+
// https://github.com/lightningnetwork/lnd/blob/master/lnwallet/btcwallet/signer.go#L286
261+
// Because we know all custom families (0 to 255) are derived at wallet
262+
// creation, and the very first index of each family/account is always
263+
// derived, we know that only using the public key for that very first
264+
// index will work. But for a freshly initialized wallet (e.g. restored
265+
// from seed), we won't know any indexes greater than 0, so we _need_ to
266+
// also specify the key locator and not just the public key.
267+
fullDescriptor := func(
268+
d keychain.KeyDescriptor) *signrpc.KeyDescriptor {
269+
270+
keyDesc := &signrpc.KeyDescriptor{}
271+
if d.PubKey != nil {
272+
keyDesc.RawKeyBytes = d.PubKey.SerializeCompressed()
273+
}
274+
275+
if d.KeyLocator.Family != 0 && d.KeyLocator.Index != 0 {
276+
keyDesc.KeyLoc = &signrpc.KeyLocator{
277+
KeyFamily: int32(d.KeyLocator.Family),
278+
KeyIndex: int32(d.KeyLocator.Index),
279+
}
280+
}
281+
282+
return keyDesc
283+
}
284+
285+
rpcSignDescs := make([]*signrpc.SignDescriptor, len(signDescriptors))
286+
for i, signDesc := range signDescriptors {
287+
keyDesc := partialDescriptor(signDesc.KeyDesc)
288+
if fullDescriptors {
289+
keyDesc = fullDescriptor(signDesc.KeyDesc)
290+
}
291+
238292
var doubleTweak []byte
239293
if signDesc.DoubleTweak != nil {
240294
doubleTweak = signDesc.DoubleTweak.Serialize()
@@ -247,12 +301,9 @@ func marshallSignDescriptors(
247301
PkScript: signDesc.Output.PkScript,
248302
Value: signDesc.Output.Value,
249303
},
250-
Sighash: uint32(signDesc.HashType),
251-
InputIndex: int32(signDesc.InputIndex),
252-
KeyDesc: &signrpc.KeyDescriptor{
253-
RawKeyBytes: keyBytes,
254-
KeyLoc: keyLocator,
255-
},
304+
Sighash: uint32(signDesc.HashType),
305+
InputIndex: int32(signDesc.InputIndex),
306+
KeyDesc: keyDesc,
256307
SingleTweak: signDesc.SingleTweak,
257308
DoubleTweak: doubleTweak,
258309
TapTweak: signDesc.TapTweak,
@@ -283,11 +334,32 @@ func (s *signerClient) SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
283334
signDescriptors []*SignDescriptor, prevOutputs []*wire.TxOut) ([][]byte,
284335
error) {
285336

337+
return s.signOutputRaw(ctx, tx, signDescriptors, prevOutputs, false)
338+
}
339+
340+
// SignOutputRawKeyLocator is a copy of the SignOutputRaw that fixes a specific
341+
// issue around how the key locator is populated in the sign descriptor. We copy
342+
// this method instead of fixing the original to make sure we don't break any
343+
// existing applications that have already adjusted themselves to use the
344+
// specific behavior of the original SignOutputRaw method.
345+
func (s *signerClient) SignOutputRawKeyLocator(ctx context.Context,
346+
tx *wire.MsgTx, signDescriptors []*SignDescriptor,
347+
prevOutputs []*wire.TxOut) ([][]byte, error) {
348+
349+
return s.signOutputRaw(ctx, tx, signDescriptors, prevOutputs, true)
350+
}
351+
352+
// signOutputRaw is a helper method that performs the actual signing of the
353+
// transaction.
354+
func (s *signerClient) signOutputRaw(ctx context.Context, tx *wire.MsgTx,
355+
signDescriptors []*SignDescriptor, prevOutputs []*wire.TxOut,
356+
fullDescriptor bool) ([][]byte, error) {
357+
286358
txRaw, err := encodeTx(tx)
287359
if err != nil {
288360
return nil, err
289361
}
290-
rpcSignDescs := marshallSignDescriptors(signDescriptors)
362+
rpcSignDescs := marshallSignDescriptors(signDescriptors, fullDescriptor)
291363
rpcPrevOutputs := marshallTxOut(prevOutputs)
292364

293365
rpcCtx, cancel := context.WithTimeout(ctx, s.timeout)
@@ -321,7 +393,7 @@ func (s *signerClient) ComputeInputScript(ctx context.Context, tx *wire.MsgTx,
321393
if err != nil {
322394
return nil, err
323395
}
324-
rpcSignDescs := marshallSignDescriptors(signDescriptors)
396+
rpcSignDescs := marshallSignDescriptors(signDescriptors, false)
325397
rpcPrevOutputs := marshallTxOut(prevOutputs)
326398

327399
rpcCtx, cancel := context.WithTimeout(ctx, s.timeout)

0 commit comments

Comments
 (0)