-
Notifications
You must be signed in to change notification settings - Fork 653
Expand file tree
/
Copy pathsigner.go
More file actions
1285 lines (1063 loc) · 40.9 KB
/
Copy pathsigner.go
File metadata and controls
1285 lines (1063 loc) · 40.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package wallet
import (
"context"
"errors"
"fmt"
"github.com/btcsuite/btcd/address/v2"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/v2/hdkeychain"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/btcsuite/btcwallet/internal/zero"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet/internal/db"
"github.com/btcsuite/btcwallet/wallet/internal/keyvault"
)
var (
// ErrUnknownSignMethod is returned when a transaction is signed with an
// unknown sign method.
ErrUnknownSignMethod = errors.New("unknown sign method")
// ErrUnsupportedAddressType is returned when a transaction is signed
// for an unsupported address type.
ErrUnsupportedAddressType = errors.New("unsupported address type")
// ErrInvalidDigestSize is returned when a signature digest is not 32
// bytes.
ErrInvalidDigestSize = errors.New("digest must be 32 bytes")
// ErrInvalidSignParam is returned when the parameters for the signing
// operation are invalid.
ErrInvalidSignParam = errors.New("invalid signing parameters")
// ErrWatchOnlyAccount is returned when account metadata exists but has
// no private key material available for signing.
ErrWatchOnlyAccount = errors.New("account is watch-only")
// ErrAccountNotInStore is returned when neither legacy waddrmgr nor the
// durable store can resolve the signing account.
ErrAccountNotInStore = errors.New("account not in store")
)
// DerivePubKeyParams identifies an account and the unhardened child key to
// derive from its extended public key.
type DerivePubKeyParams struct {
// Account identifies the account by portable wallet semantics.
Account AccountSelector
// Branch is the account child branch to derive.
Branch uint32
// Index is the child index within Branch to derive.
Index uint32
}
// Signer provides an interface for common, safe cryptographic operations,
// including signing and key derivation.
type Signer interface {
// DerivePubKey derives a public child key from the selected account's
// extended public key. The account may be selected by name when it has
// no BIP44 account number, such as an imported XPub account.
DerivePubKey(ctx context.Context, params DerivePubKeyParams) (
*btcec.PublicKey, error)
// ECDH performs a scalar multiplication (ECDH-like operation) between
// a key from the wallet and a remote public key. The output returned
// will be the raw 32-byte shared secret (the X-coordinate of the
// result point).
ECDH(ctx context.Context, path BIP32Path, pub *btcec.PublicKey) (
[32]byte, error)
// SignDigest signs a message digest based on the provided intent. The
// returned Signature is a marker interface that can be asserted to the
// concrete signature types, ECDSASignature or SchnorrSignature.
SignDigest(ctx context.Context, path BIP32Path,
intent *SignDigestIntent) (Signature, error)
// ComputeUnlockingScript generates the full sigScript and witness
// required to spend a UTXO. The resulting UnlockingScript struct
// contains the raw witness and/or sigScript, which can be used to
// populate the final transaction input.
//
// This method is designed for spending single-signature outputs, which
// are outputs that can be spent with a single signature from a single
// private key. This includes P2PKH, P2WKH, NP2WKH, and P2TR key-path
// spends. For more complex script-based spends, such as P2SH or P2WSH
// multisig, the ComputeRawSig method should be used to generate the raw
// signature, which can then be manually assembled into the final
// witness.
ComputeUnlockingScript(ctx context.Context,
params *UnlockingScriptParams) (*UnlockingScript, error)
// ComputeRawSig generates a raw signature for a single transaction
// input. The caller is responsible for assembling the final witness.
//
// This method is a low-level specialist function that should only be
// used when the caller needs to generate a raw signature for a
// specific key, without the wallet assembling the final witness. This
// is useful for multi-party protocols like multisig or Lightning,
// where signatures may need to be exchanged and combined before the
// final witness is created. For most common, single-signature spends,
// ComputeUnlockingScript should be used instead.
ComputeRawSig(ctx context.Context, params *RawSigParams) (
RawSignature, error)
}
// UnsafeSigner provides an interface for security-sensitive cryptographic
// operations that export raw private key material. This interface should be
// used with extreme care and only when absolutely necessary.
type UnsafeSigner interface {
Signer
// DerivePrivKey derives a private key from a full BIP-32 derivation
// path.
//
// DANGER: This method exports sensitive key material.
DerivePrivKey(ctx context.Context, path BIP32Path) (
*btcec.PrivateKey, error)
// GetPrivKeyForAddress returns the private key for a given address.
//
// DANGER: This method exports sensitive key material.
GetPrivKeyForAddress(ctx context.Context, a address.Address) (
*btcec.PrivateKey, error)
}
// Compile-time checks ensure that Wallet implements the signer interfaces.
var _ Signer = (*Wallet)(nil)
var _ UnsafeSigner = (*Wallet)(nil)
// BIP32Path contains the full information needed to derive a key from the
// wallet's master seed, as defined by BIP-32. It combines the high-level key
// scope with the specific derivation path.
type BIP32Path struct {
// KeyScope specifies the key scope (e.g., P2WKH, P2TR, or lnd's custom
// scope).
KeyScope waddrmgr.KeyScope
// DerivationPath specifies the full derivation path within the scope.
DerivationPath waddrmgr.DerivationPath
}
// SignatureType represents the type of signature to produce.
type SignatureType uint8
const (
// SigTypeECDSA represents an ECDSA signature.
SigTypeECDSA SignatureType = iota
// SigTypeSchnorr represents a Schnorr signature.
SigTypeSchnorr
)
// SignDigestIntent represents the user's intent to sign a message digest. It
// serves as a blueprint for the Signer, bundling all the parameters
// required to produce a signature into a single, coherent structure.
//
// # Usage Examples
//
// ## Standard ECDSA Signature (DER Encoded)
// To produce a standard ECDSA signature, set SigType to SigTypeECDSA.
//
// intent := &wallet.SignDigestIntent{
// Digest: chainhash.HashB([]byte("a message")),
// SigType: wallet.SigTypeECDSA,
// }
// rawSig, err := signer.SignDigest(ctx, path, intent)
// // Type-assert the result to ECDSASignature.
// ecdsaSig := rawSig.(wallet.ECDSASignature)
//
// ## Compact, Recoverable ECDSA Signature
// To produce a compact, recoverable signature, set CompactSig to true.
//
// intent := &wallet.SignDigestIntent{
// Digest: chainhash.DoubleHashB([]byte("a message")),
// SigType: wallet.SigTypeECDSA,
// CompactSig: true,
// }
// rawSig, err := signer.SignDigest(ctx, path, intent)
// // Type-assert the result to CompactSignature.
// compactSig := rawSig.(wallet.CompactSignature)
//
// ## Schnorr Signature
// To produce a Schnorr signature, set SigType to SigTypeSchnorr.
//
// intent := &wallet.SignDigestIntent{
// Digest: chainhash.TaggedHash(
// []byte("my_protocol_tag"), []byte("a message"),
// ),
// SigType: wallet.SigTypeSchnorr,
// }
// rawSig, err := signer.SignDigest(ctx, path, intent)
// // Type-assert the result to SchnorrSignature.
// schnorrSig := rawSig.(wallet.SchnorrSignature)
type SignDigestIntent struct {
// Digest is the 32-byte hash digest to be signed.
Digest []byte
// SigType specifies the type of signature to generate.
SigType SignatureType
// CompactSig specifies whether the signature should be returned in the
// compact, recoverable format. This is only valid for ECDSA signatures.
CompactSig bool
// TaprootTweak is an optional private key tweak to be applied before
// signing. This is only valid for Schnorr signatures.
TaprootTweak []byte
}
// Signature is an interface that represents a cryptographic signature.
// It is a marker interface to allow returning different signature types.
type Signature interface {
// isSignature is a marker method to ensure that only the types defined
// in this package can implement this interface.
isSignature()
}
// ECDSASignature wraps an ecdsa.Signature to implement the Signature interface.
type ECDSASignature struct {
*ecdsa.Signature
}
// CompactSignature wraps a compact signature byte slice to implement the
// Signature interface.
type CompactSignature []byte
// SchnorrSignature wraps a schnorr.Signature to implement the Signature
// interface.
type SchnorrSignature struct {
*schnorr.Signature
}
// isSignature implements the Signature marker interface.
func (ECDSASignature) isSignature() {}
// isSignature implements the Signature marker interface.
func (CompactSignature) isSignature() {}
// isSignature implements the Signature marker interface.
func (SchnorrSignature) isSignature() {}
// UnlockingScript is a struct that contains the witness and sigScript for a
// transaction input.
type UnlockingScript struct {
// Witness is the witness stack for the input. For non-SegWit inputs,
// this will be nil.
Witness wire.TxWitness
// SigScript is the signature script for the input. For native SegWit
// inputs, this will be nil.
SigScript []byte
}
// PrivKeyTweaker is a function type that can be used to pass in a callback for
// tweaking a private key before it's used to sign an input.
type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error)
// UnlockingScriptParams provides all the necessary parameters to generate an
// unlocking script (witness and sigScript) for a transaction input.
type UnlockingScriptParams struct {
// Tx is the transaction containing the input to be signed.
Tx *wire.MsgTx
// InputIndex is the index of the input to be signed.
InputIndex int
// Output is the previous output that is being spent.
Output *wire.TxOut
// SigHashes is the sighash cache for the transaction.
SigHashes *txscript.TxSigHashes
// HashType is the signature hash type to use.
HashType txscript.SigHashType
// Tweaker is an optional function that can be used to tweak the
// private key before signing.
Tweaker PrivKeyTweaker
}
// RawSigParams provides all the necessary parameters to generate a raw
// signature for a transaction input.
type RawSigParams struct {
// Tx is the transaction containing the input to be signed.
Tx *wire.MsgTx
// InputIndex is the index of the input to be signed.
InputIndex int
// Output is the previous output that is being spent.
Output *wire.TxOut
// SigHashes is the sighash cache for the transaction.
SigHashes *txscript.TxSigHashes
// HashType is the signature hash type to use.
HashType txscript.SigHashType
// Path is the BIP-32 derivation path of the key to be used for
// signing.
Path BIP32Path
// Tweaker is an optional function that can be used to tweak the
// private key before signing.
Tweaker PrivKeyTweaker
// Details specifies the version-specific information for signing.
// This field MUST be set to either LegacySpendDetails,
// SegwitV0SpendDetails or TaprootSpendDetails.
Details SpendDetails
}
// RawSignature is a raw signature.
type RawSignature []byte
// TaprootSpendPath is an enum that specifies the spending path to be used for a
// Taproot input.
type TaprootSpendPath uint8
const (
// KeyPathSpend indicates that the output should be spent using the key
// path.
KeyPathSpend TaprootSpendPath = iota
// ScriptPathSpend indicates that the output should be spent using the
// script path.
ScriptPathSpend
)
// SpendDetails is a sealed interface that provides the version-specific
// details required to generate a raw signature.
type SpendDetails interface {
// isSpendDetails is a marker method to ensure that only the types
// defined in this package can implement this interface.
isSpendDetails()
// Sign performs the version-specific signing operation.
Sign(params *RawSigParams, privKey *btcec.PrivateKey) (
RawSignature, error)
}
// LegacySpendDetails provides the details for signing a legacy P2PKH input.
type LegacySpendDetails struct {
// RedeemScript is the redeem script for P2SH spends.
RedeemScript []byte
}
// Sign performs the version-specific signing operation for a legacy input.
func (l LegacySpendDetails) Sign(params *RawSigParams,
privKey *btcec.PrivateKey) (RawSignature, error) {
// For P2SH, the redeem script must be provided. For P2PKH, the pkscript
// of the output is used.
script := l.RedeemScript
if script == nil {
script = params.Output.PkScript
}
rawSig, err := txscript.RawTxInSignature(
params.Tx, params.InputIndex, script,
params.HashType, privKey,
)
if err != nil {
return nil, fmt.Errorf("cannot create raw signature: %w", err)
}
return rawSig, nil
}
// isSpendDetails implements the sealed interface.
func (l LegacySpendDetails) isSpendDetails() {}
// SegwitV0SpendDetails provides the details for signing a SegWit v0 input.
type SegwitV0SpendDetails struct {
// WitnessScript is the witness script for P2WSH spends. For P2WKH,
// this should be the P2PKH script of the key.
WitnessScript []byte
}
// Sign performs the version-specific signing operation for a SegWit v0 input.
func (s SegwitV0SpendDetails) Sign(params *RawSigParams,
privKey *btcec.PrivateKey) (RawSignature, error) {
sig, err := txscript.RawTxInWitnessSignature(
params.Tx, params.SigHashes, params.InputIndex,
params.Output.Value, s.WitnessScript,
params.HashType, privKey,
)
if err != nil {
return nil, fmt.Errorf("cannot create witness sig: %w", err)
}
// Validate the signature by parsing it. This serves as a sanity check
// to ensure the generated signature is valid.
_, err = ecdsa.ParseDERSignature(sig[:len(sig)-1])
if err != nil {
return nil, fmt.Errorf("generated invalid witness sig: %w", err)
}
return sig[:len(sig)-1], nil
}
// isSpendDetails implements the sealed interface.
func (s SegwitV0SpendDetails) isSpendDetails() {}
// TaprootSpendDetails provides the details for signing a Taproot input.
type TaprootSpendDetails struct {
// SpendPath specifies which spending path to use.
SpendPath TaprootSpendPath
// Tweak is the tweak to apply to the internal key. For a key-path
// spend, this is typically the merkle root of the script tree.
Tweak []byte
// WitnessScript is the specific script leaf being spent. This is
// only used for ScriptPathSpend.
WitnessScript []byte
}
// Sign performs the version-specific signing operation for a Taproot input.
func (t TaprootSpendDetails) Sign(params *RawSigParams,
privKey *btcec.PrivateKey) (RawSignature, error) {
var (
rawSig []byte
err error
)
switch t.SpendPath {
case KeyPathSpend:
rawSig, err = txscript.RawTxInTaprootSignature(
params.Tx, params.SigHashes,
params.InputIndex, params.Output.Value,
params.Output.PkScript, t.Tweak,
params.HashType, privKey,
)
if err != nil {
return nil, fmt.Errorf("taproot sig error: %w", err)
}
case ScriptPathSpend:
leaf := txscript.TapLeaf{
LeafVersion: txscript.BaseLeafVersion,
Script: t.WitnessScript,
}
rawSig, err = txscript.RawTxInTapscriptSignature(
params.Tx, params.SigHashes,
params.InputIndex, params.Output.Value,
params.Output.PkScript, leaf,
params.HashType, privKey,
)
if err != nil {
return nil, fmt.Errorf("tapscript sig error: %w", err)
}
default:
return nil, fmt.Errorf("%w: %v", ErrUnknownSignMethod,
t.SpendPath)
}
// Validate the signature by parsing it. This serves as a sanity check
// to ensure the generated signature is valid.
_, err = schnorr.ParseSignature(rawSig[:schnorr.SignatureSize])
if err != nil {
return nil, fmt.Errorf("generated invalid taproot sig: %w", err)
}
return rawSig, nil
}
// isSpendDetails implements the sealed interface.
func (t TaprootSpendDetails) isSpendDetails() {}
// A compile-time assertion to ensure that all SpendDetails implementations
// adhere to the interface.
var _ SpendDetails = (*LegacySpendDetails)(nil)
var _ SpendDetails = (*SegwitV0SpendDetails)(nil)
var _ SpendDetails = (*TaprootSpendDetails)(nil)
// DerivePubKey derives a public child key from a semantically selected
// account. The account XPub is read from the durable store, then the branch and
// child index are derived in memory. Public derivation requires a started
// wallet but remains available while the wallet is locked.
func (w *Wallet) DerivePubKey(ctx context.Context,
params DerivePubKeyParams) (
*btcec.PublicKey, error) {
err := w.state.validateStarted()
if err != nil {
return nil, err
}
err = params.Account.validate()
if err != nil {
return nil, err
}
return w.resolveDerivedPubKeyFromStore(ctx, params)
}
// derivePathPrivKey resolves the signing private key for a full BIP-32 path.
//
// The private key is resolved entirely through the durable store: the
// account-level extended private key is fetched by the path's BIP44 account
// number, decrypted through keyVault, and the branch and index derived
// locally. This single path covers both SQL-backed and kvdb-backed wallets
// because the kvdb store adapter exports the legacy address manager's
// encrypted account material through the same account-secret contract.
//
// This resolver is used by the path-driven signer entry points (SignDigest,
// ECDH, DerivePrivKey and ComputeRawSig/PSBT), which have only a derivation
// path. Address-driven paths reach the same resolver through
// privKeyForAddressInfo once the store has resolved the address to its
// derivation info, so both address the account by number.
//
// The returned private key is owned by the caller, who is responsible for
// zeroing it once signing completes.
func (w *Wallet) derivePathPrivKey(ctx context.Context, path BIP32Path) (
*btcec.PrivateKey, error) {
return w.resolveDerivedPrivKeyFromStore(
ctx, path.KeyScope, path.DerivationPath,
)
}
// ECDH performs a scalar multiplication (ECDH-like operation) between a key
// from the wallet and a remote public key. The output returned will be the
// sha256 of the resulting shared point serialized in compressed format.
func (w *Wallet) ECDH(ctx context.Context, path BIP32Path,
pub *btcec.PublicKey) ([32]byte, error) {
err := w.state.canSign()
if err != nil {
return [32]byte{}, err
}
privKey, err := w.derivePathPrivKey(ctx, path)
if err != nil {
return [32]byte{}, err
}
defer privKey.Zero()
// Perform the scalar multiplication and hash the result.
secret := btcec.GenerateSharedSecret(privKey, pub)
var sharedSecret [32]byte
copy(sharedSecret[:], secret)
return sharedSecret, nil
}
// validateSignDigestIntent validates the parameters of a SignDigestIntent.
func validateSignDigestIntent(intent *SignDigestIntent) error {
// The digest must be exactly 32 bytes.
if len(intent.Digest) != chainhash.HashSize {
return ErrInvalidDigestSize
}
// Validate parameters based on signature type.
switch intent.SigType {
case SigTypeECDSA:
if intent.TaprootTweak != nil {
return fmt.Errorf("%w: taproot tweak cannot be used "+
"with ECDSA", ErrInvalidSignParam)
}
case SigTypeSchnorr:
if intent.CompactSig {
return fmt.Errorf("%w: compact signature cannot be "+
"used with Schnorr", ErrInvalidSignParam)
}
}
return nil
}
// SignDigest signs a message digest based on the provided intent.
func (w *Wallet) SignDigest(ctx context.Context, path BIP32Path,
intent *SignDigestIntent) (Signature, error) {
err := w.state.canSign()
if err != nil {
return nil, err
}
err = validateSignDigestIntent(intent)
if err != nil {
return nil, err
}
privKey, err := w.derivePathPrivKey(ctx, path)
if err != nil {
return nil, err
}
defer privKey.Zero()
// Now, sign the message using the derived private key. This is all
// pure computation, so it can be done outside the DB transaction.
return signDigestWithPrivKey(privKey, intent)
}
// signDigestWithPrivKey performs the actual signing of a digest with a given
// private key, based on the options specified in the SignDigestIntent. It
// acts as a dispatcher to the appropriate signing algorithm.
func signDigestWithPrivKey(privKey *btcec.PrivateKey,
intent *SignDigestIntent) (Signature, error) {
// If Schnorr is specified, we'll generate a Schnorr signature.
if intent.SigType == SigTypeSchnorr {
return signDigestSchnorr(privKey, intent)
}
// Otherwise, we'll generate an ECDSA signature.
return signDigestECDSA(privKey, intent)
}
// signDigestSchnorr performs the actual signing of a digest with a given
// private key, using the Schnorr signature algorithm.
func signDigestSchnorr(privKey *btcec.PrivateKey,
intent *SignDigestIntent) (Signature, error) {
if intent.TaprootTweak != nil {
privKey = txscript.TweakTaprootPrivKey(
*privKey, intent.TaprootTweak,
)
}
sig, err := schnorr.Sign(privKey, intent.Digest)
if err != nil {
return nil, fmt.Errorf("cannot create schnorr sig: %w", err)
}
return SchnorrSignature{sig}, nil
}
// signDigestECDSA performs the actual signing of a digest with a given
// private key, using the ECDSA signature algorithm.
func signDigestECDSA(privKey *btcec.PrivateKey,
intent *SignDigestIntent) (Signature, error) {
if intent.CompactSig {
sig := ecdsa.SignCompact(privKey, intent.Digest, true)
return CompactSignature(sig), nil
}
sig := ecdsa.Sign(privKey, intent.Digest)
return ECDSASignature{sig}, nil
}
// ComputeUnlockingScript generates the full sigScript and witness required to
// spend a UTXO.
func (w *Wallet) ComputeUnlockingScript(ctx context.Context,
params *UnlockingScriptParams) (*UnlockingScript, error) {
err := w.state.canSign()
if err != nil {
return nil, err
}
// First, we'll fetch the managed address that corresponds to the
// output being spent. This will be used to look up the private key
// required for signing.
scriptInfo, err := w.ScriptForOutput(ctx, *params.Output)
if err != nil {
return nil, err
}
privKey, err := w.privKeyForOutput(ctx, scriptInfo)
if err != nil {
return nil, err
}
defer privKey.Zero()
// If a tweaker is provided, we'll use it to tweak the private key.
if params.Tweaker != nil {
privKey, err = params.Tweaker(privKey)
if err != nil {
return nil, fmt.Errorf("error tweaking private key: %w",
err)
}
}
// With the private key retrieved and tweaked, we can now generate the
// unlocking script.
return signAndAssembleScript(params, privKey, &scriptInfo)
}
// privKeyForOutput returns the private key needed to sign for the given
// wallet-controlled output.
//
// Derived addresses resolve through the account-level secret. A derived child
// without wallet-seed derivation metadata, such as an imported-XPub child,
// cannot sign. Only raw imported addresses resolve through their own encrypted
// private key material in the store.
func (w *Wallet) privKeyForOutput(ctx context.Context,
scriptInfo OutputScriptInfo) (
*btcec.PrivateKey, error) {
if canUseAddressInfoDerivation(scriptInfo.AddressInfo) {
return w.privKeyForAddressInfo(ctx, scriptInfo.AddressInfo)
}
if !scriptInfo.Imported {
return nil, ErrNoAssocPrivateKey
}
return w.resolveImportedAddrPrivKey(ctx, scriptInfo.scriptPubKey())
}
// scriptPubKey returns the output pkScript associated with the address
// metadata. It re-derives the script from the address so callers that only
// hold OutputScriptInfo need not thread the raw pkScript separately.
func (info OutputScriptInfo) scriptPubKey() []byte {
// The script is only used to key the store lookup. A derivation error
// returns nil, so the subsequent query fails address-query validation.
script, err := txscript.PayToAddrScript(info.Addr)
if err != nil {
return nil
}
return script
}
// canUseAddressInfoDerivation reports whether address metadata contains enough
// derivation information to derive a private key without a legacy address row.
func canUseAddressInfoDerivation(addressInfo AddressInfo) bool {
if addressInfo.Imported || addressInfo.Derivation == nil {
return false
}
return addressInfo.Derivation.KeyScope != (waddrmgr.KeyScope{})
}
// privKeyForAddressInfo derives the private key described by store-backed
// address metadata. It resolves the owning account's encrypted extended
// private key through the store, decrypts it through keyVault, and derives the
// leaf key at the address's branch and index.
func (w *Wallet) privKeyForAddressInfo(ctx context.Context,
addressInfo AddressInfo) (
*btcec.PrivateKey, error) {
// An address with no derivation is either a raw single import or an
// imported-xpub child. Neither has wallet-derived account-level private
// material to resolve, so refuse before any secret lookup rather than
// reading a missing account number as account 0, which is the wallet's
// own default derived account.
derivation := addressInfo.Derivation
if derivation == nil {
return nil, ErrNoAssocPrivateKey
}
internalAccount := derivation.Account
hardenedAccount := internalAccount + hdkeychain.HardenedKeyStart
derivationPath := waddrmgr.DerivationPath{
InternalAccount: internalAccount,
Account: hardenedAccount,
Branch: derivation.Branch,
Index: derivation.Index,
MasterKeyFingerprint: derivation.MasterKeyFingerprint,
}
return w.resolveDerivedPrivKeyFromStore(
ctx, derivation.KeyScope, derivationPath,
)
}
// resolveImportedAddrPrivKey resolves the private key for an imported address
// from its encrypted private-key material in the store. Imported addresses
// have no derivation path, so the key is stored per-address rather than
// derived from an account. An address that exists but holds no private-key
// material (watch-only import) yields ErrNoAssocPrivateKey.
func (w *Wallet) resolveImportedAddrPrivKey(ctx context.Context,
scriptPubKey []byte) (*btcec.PrivateKey, error) {
secret, err := w.cache.GetAddressSecret(ctx, db.GetAddressSecretQuery{
WalletID: w.id,
ScriptPubKey: scriptPubKey,
})
switch {
// A resolved address that carries no secret, and (for kvdb) an address
// that does not resolve at all, both surface as ErrSecretNotFound.
// Either way the wallet holds no spendable key for this address.
case errors.Is(err, db.ErrSecretNotFound),
errors.Is(err, db.ErrAddressNotFound):
return nil, ErrNoAssocPrivateKey
case err != nil:
return nil, fmt.Errorf("fetch address secret: %w", err)
}
if len(secret.EncryptedPrivKey) == 0 {
return nil, ErrNoAssocPrivateKey
}
plaintext, err := w.keyVault.Decrypt(
waddrmgr.CKTPrivate, secret.EncryptedPrivKey,
)
if err != nil {
return nil, fmt.Errorf("decrypt imported priv: %w", err)
}
privKey, _ := btcec.PrivKeyFromBytes(plaintext)
zero.Bytes(plaintext)
return privKey, nil
}
// isScriptSpendAddress reports whether an address is spent through a redeem or
// witness script rather than a single public key. These are the P2SH, P2WSH
// and taproot script-path families, whose spending script is stored encrypted
// per address rather than derived from a public key.
func isScriptSpendAddress(addressInfo AddressInfo) bool {
spendType := addressInfo.AddrType.SpendType()
return spendType == waddrmgr.SpendTypeScriptHash ||
spendType == waddrmgr.SpendTypeWitnessScript ||
spendType == waddrmgr.SpendTypeTaprootScriptPath
}
// scriptForAddressInfo resolves the plaintext redeem or witness script for a
// script-based output from its encrypted material in the store. The stored
// script is decrypted through the key vault under the script crypto key. For
// taproot script-path addresses the stored blob is a TLV-encoded Tapscript, so
// it is decoded and the revealed leaf script returned; for P2SH and P2WSH the
// decrypted bytes are the redeem or witness script directly.
//
// An address that exists but carries no encrypted script (a watch-only import)
// yields ErrNoAssocPrivateKey, matching the private-key surface: the wallet
// cannot spend it.
func (w *Wallet) scriptForAddressInfo(ctx context.Context,
addressInfo AddressInfo, scriptPubKey []byte) ([]byte, error) {
secret, err := w.cache.GetAddressSecret(ctx, db.GetAddressSecretQuery{
WalletID: w.id,
ScriptPubKey: scriptPubKey,
})
switch {
case errors.Is(err, db.ErrSecretNotFound),
errors.Is(err, db.ErrAddressNotFound):
return nil, ErrNoAssocPrivateKey
case err != nil:
return nil, fmt.Errorf("fetch address secret: %w", err)
}
if len(secret.EncryptedScript) == 0 {
return nil, ErrNoAssocPrivateKey
}
// Decrypt the script under the key the store reports it was written
// with. waddrmgr records that per address: a non-secret witness or
// taproot row is sealed under the public key and stays readable while
// locked, everything else under the script key.
scriptKey := waddrmgr.CKTPublic
if secret.ScriptIsSecret {
scriptKey = waddrmgr.CKTScript
}
plaintext, err := w.keyVault.Decrypt(scriptKey, secret.EncryptedScript)
if err != nil {
return nil, fmt.Errorf("decrypt script: %w", err)
}
// Non-taproot script families store the redeem or witness script
// directly, so the decrypted bytes are the script itself.
if addressInfo.AddrType.SpendType() !=
waddrmgr.SpendTypeTaprootScriptPath {
return plaintext, nil
}
// Taproot script-path imports store a TLV-encoded Tapscript; decode it
// and return the single revealed leaf script.
tapscript, err := waddrmgr.DecodeTaprootScript(plaintext)
if err != nil {
return nil, fmt.Errorf("decode tapscript: %w", err)
}
script, err := revealedTapscriptLeaf(tapscript)
if err != nil {
return nil, fmt.Errorf("%w: addr %v", err, addressInfo.Addr)
}
return script, nil
}
// revealedTapscriptLeaf returns the single leaf script a taproot script-path
// spend commits to. It supports the partial-reveal form (one revealed script)
// and the full-tree form when the tree holds exactly one leaf; other shapes do
// not carry a unique spending script and are rejected.
func revealedTapscriptLeaf(tapscript *waddrmgr.Tapscript) ([]byte, error) {
switch {
case len(tapscript.RevealedScript) > 0:
return tapscript.RevealedScript, nil
case len(tapscript.Leaves) == 1:
return tapscript.Leaves[0].Script, nil
default:
return nil, ErrDerivationPathNotFound
}
}
// resolveDerivedPrivKeyFromStore resolves one derived private key from the
// account-level encrypted secret stored behind the wallet store, addressed by
// the derivation path's BIP44 account number.
//
// One selector suffices because derived accounts are the only kind holding
// account-level signing material: an imported account is created from an
// extended public key, so callers reaching an imported-xpub child must refuse
// before arriving here rather than falling back to account 0.
//
// A watch-only account (no encrypted private material) yields
// ErrWatchOnlyAccount, and an account that is not in the store yields
// ErrAccountNotInStore. The returned private key is owned by the caller, who
// is responsible for zeroing it once signing completes.
func (w *Wallet) resolveDerivedPrivKeyFromStore(ctx context.Context,
keyScope waddrmgr.KeyScope,
path waddrmgr.DerivationPath) (*btcec.PrivateKey, error) {
query := db.GetAccountSecretQuery{
WalletID: w.id,
Scope: db.KeyScope(keyScope),
AccountNumber: path.InternalAccount,
}
secret, err := w.cache.GetAccountSecret(ctx, query)
switch {
case errors.Is(err, db.ErrAccountSecretUnavailable),
errors.Is(err, db.ErrAccountNotFound):
return nil, ErrAccountNotInStore
case err != nil:
return nil, fmt.Errorf("fetch account secret: %w", err)
}
if len(secret.EncryptedPrivateKey) == 0 {
return nil, ErrWatchOnlyAccount
}
return deriveStoredAccountChildKey(
w.keyVault, secret.EncryptedPrivateKey, path,
)
}
// deriveStoredAccountChildKey decrypts an account's encrypted private key with
// the wallet's keyVault and walks the branch and index derivation to produce
// the leaf private key. The decrypted byte slice and intermediate HD keys are
// zeroed before the call returns. Note that hdkeychain/base58 parsing allocates
// a transient immutable string copy of the decrypted bytes that cannot be
// wiped and is left to the garbage collector.
func deriveStoredAccountChildKey(vault keyvault.Vault,
encryptedAccountPriv []byte,
path waddrmgr.DerivationPath) (*btcec.PrivateKey, error) {
plaintext, err := vault.Decrypt(
waddrmgr.CKTPrivate, encryptedAccountPriv,
)
if err != nil {
return nil, fmt.Errorf("decrypt account priv: %w", err)
}
acctPriv, err := hdkeychain.NewKeyFromString(string(plaintext))
if err != nil {
zero.Bytes(plaintext)
return nil, fmt.Errorf("parse account priv: %w", err)
}
zero.Bytes(plaintext)
defer acctPriv.Zero()
branchKey, err := deriveChildKey(acctPriv, path.Branch)
if err != nil {
return nil, fmt.Errorf("derive branch: %w", err)
}
defer branchKey.Zero()
addrKey, err := deriveChildKey(branchKey, path.Index)
if err != nil {
return nil, fmt.Errorf("derive index: %w", err)
}
defer addrKey.Zero()
privKey, err := addrKey.ECPrivKey()
if err != nil {
return nil, fmt.Errorf("derive private key: %w", err)
}
return privKey, nil
}