@@ -552,28 +552,65 @@ func (w *Wallet) fetchManagedPubKeyAddress(path BIP32Path) (
552552 return managedPubKeyAddr , nil
553553}
554554
555+ // derivePathPrivKey resolves the signing private key for a full BIP-32 path.
556+ //
557+ // It first walks the legacy waddrmgr-backed managed-address lookup, which is
558+ // the fast path for accounts mirrored into waddrmgr. When that lookup misses
559+ // because the account or its scope only lives in the SQL store, it falls back
560+ // to the account-level encrypted secret resolved through keyVault. The
561+ // fallback is gated on a waddrmgr account/scope miss so legacy-backed accounts
562+ // keep their existing behavior and only genuine store-only accounts take the
563+ // slower path.
564+ //
565+ // The returned private key is owned by the caller, who is responsible for
566+ // zeroing it once signing completes.
567+ func (w * Wallet ) derivePathPrivKey (ctx context.Context , path BIP32Path ) (
568+ * btcec.PrivateKey , error ) {
569+
570+ managedPubKeyAddr , err := w .fetchManagedPubKeyAddress (path )
571+ switch {
572+ case err == nil :
573+ privKey , err := managedPubKeyAddr .PrivKey ()
574+ if err != nil {
575+ return nil , fmt .Errorf ("cannot get private key: %w" , err )
576+ }
577+
578+ return privKey , nil
579+
580+ case isWaddrmgrAccountClassError (
581+ err , waddrmgr .ErrScopeNotFound , waddrmgr .ErrAccountNotFound ,
582+ ):
583+
584+ privKey , storeErr := w .resolveDerivedPrivKeyFromStore (
585+ ctx , path .KeyScope , path .DerivationPath ,
586+ )
587+ if storeErr != nil {
588+ return nil , fmt .Errorf ("store account fallback after " +
589+ "legacy address miss: %w: %w" , err , storeErr )
590+ }
591+
592+ return privKey , nil
593+
594+ default :
595+ return nil , err
596+ }
597+ }
598+
555599// ECDH performs a scalar multiplication (ECDH-like operation) between a key
556600// from the wallet and a remote public key. The output returned will be the
557601// sha256 of the resulting shared point serialized in compressed format.
558- func (w * Wallet ) ECDH (_ context.Context , path BIP32Path ,
602+ func (w * Wallet ) ECDH (ctx context.Context , path BIP32Path ,
559603 pub * btcec.PublicKey ) ([32 ]byte , error ) {
560604
561605 err := w .state .canSign ()
562606 if err != nil {
563607 return [32 ]byte {}, err
564608 }
565609
566- managedPubKeyAddr , err := w .fetchManagedPubKeyAddress ( path )
610+ privKey , err := w .derivePathPrivKey ( ctx , path )
567611 if err != nil {
568612 return [32 ]byte {}, err
569613 }
570-
571- // Get the private key for the derived address.
572- privKey , err := managedPubKeyAddr .PrivKey ()
573- if err != nil {
574- return [32 ]byte {}, fmt .Errorf ("cannot get private key: %w" ,
575- err )
576- }
577614 defer privKey .Zero ()
578615
579616 // Perform the scalar multiplication and hash the result.
@@ -611,7 +648,7 @@ func validateSignDigestIntent(intent *SignDigestIntent) error {
611648}
612649
613650// SignDigest signs a message digest based on the provided intent.
614- func (w * Wallet ) SignDigest (_ context.Context , path BIP32Path ,
651+ func (w * Wallet ) SignDigest (ctx context.Context , path BIP32Path ,
615652 intent * SignDigestIntent ) (Signature , error ) {
616653
617654 err := w .state .canSign ()
@@ -624,16 +661,10 @@ func (w *Wallet) SignDigest(_ context.Context, path BIP32Path,
624661 return nil , err
625662 }
626663
627- managedPubKeyAddr , err := w .fetchManagedPubKeyAddress ( path )
664+ privKey , err := w .derivePathPrivKey ( ctx , path )
628665 if err != nil {
629666 return nil , err
630667 }
631-
632- // Get the private key for the derived address.
633- privKey , err := managedPubKeyAddr .PrivKey ()
634- if err != nil {
635- return nil , fmt .Errorf ("cannot get private key: %w" , err )
636- }
637668 defer privKey .Zero ()
638669
639670 // Now, sign the message using the derived private key. This is all
@@ -708,7 +739,7 @@ func (w *Wallet) ComputeUnlockingScript(ctx context.Context,
708739 return nil , err
709740 }
710741
711- privKey , err := w .privKeyForOutput (scriptInfo )
742+ privKey , err := w .privKeyForOutput (ctx , scriptInfo )
712743 if err != nil {
713744 return nil , err
714745 }
@@ -730,19 +761,20 @@ func (w *Wallet) ComputeUnlockingScript(ctx context.Context,
730761
731762// privKeyForOutput returns the private key needed to sign for the given
732763// wallet-controlled output.
733- func (w * Wallet ) privKeyForOutput (scriptInfo OutputScriptInfo ) (
764+ func (w * Wallet ) privKeyForOutput (ctx context.Context ,
765+ scriptInfo OutputScriptInfo ) (
734766 * btcec.PrivateKey , error ) {
735767
736768 if canUseAddressInfoDerivation (scriptInfo .AddressInfo ) {
737- return w .privKeyForAddressInfo (scriptInfo .AddressInfo )
769+ return w .privKeyForAddressInfo (ctx , scriptInfo .AddressInfo )
738770 }
739771
740772 pubKeyAddr , err := w .loadManagedPubKeyAddr (scriptInfo .Addr )
741773 if err != nil {
742774 return nil , err
743775 }
744776
745- return w .resolvePrivKey (pubKeyAddr )
777+ return w .resolvePrivKey (ctx , pubKeyAddr )
746778}
747779
748780// canUseAddressInfoDerivation reports whether address metadata contains enough
@@ -757,7 +789,8 @@ func canUseAddressInfoDerivation(addressInfo AddressInfo) bool {
757789
758790// privKeyForAddressInfo derives the private key described by store-backed
759791// address metadata.
760- func (w * Wallet ) privKeyForAddressInfo (addressInfo AddressInfo ) (
792+ func (w * Wallet ) privKeyForAddressInfo (ctx context.Context ,
793+ addressInfo AddressInfo ) (
761794 * btcec.PrivateKey , error ) {
762795
763796 derivation := addressInfo .Derivation
@@ -774,7 +807,9 @@ func (w *Wallet) privKeyForAddressInfo(addressInfo AddressInfo) (
774807 MasterKeyFingerprint : derivation .MasterKeyFingerprint ,
775808 }
776809
777- return w .resolveDerivedPathPrivKey (derivation .KeyScope , derivationPath )
810+ return w .resolveDerivedPathPrivKey (
811+ ctx , derivation .KeyScope , derivationPath ,
812+ )
778813}
779814
780815// loadManagedPubKeyAddr loads a managed pubkey address for signer-private key
@@ -811,7 +846,8 @@ func (w *Wallet) loadManagedPubKeyAddr(addr address.Address) (
811846
812847// resolvePrivKey resolves the private key for a managed pubkey address without
813848// using output-script inspection as the private-key lookup seam.
814- func (w * Wallet ) resolvePrivKey (pubKeyAddr waddrmgr.ManagedPubKeyAddress ) (
849+ func (w * Wallet ) resolvePrivKey (ctx context.Context ,
850+ pubKeyAddr waddrmgr.ManagedPubKeyAddress ) (
815851 * btcec.PrivateKey , error ) {
816852
817853 // Imported spendable keys have no derivation path, so we fall back to the
@@ -831,25 +867,39 @@ func (w *Wallet) resolvePrivKey(pubKeyAddr waddrmgr.ManagedPubKeyAddress) (
831867 pubKeyAddr .Address ())
832868 }
833869
834- return w .resolveDerivedPathPrivKey (keyScope , derivationPath )
870+ return w .resolveDerivedPathPrivKey (ctx , keyScope , derivationPath )
835871}
836872
837873// resolveDerivedPathPrivKey resolves one derived private key through the scoped
838874// manager cache or the database-backed fallback.
839- func (w * Wallet ) resolveDerivedPathPrivKey (keyScope waddrmgr.KeyScope ,
875+ func (w * Wallet ) resolveDerivedPathPrivKey (ctx context.Context ,
876+ keyScope waddrmgr.KeyScope ,
840877 derivationPath waddrmgr.DerivationPath ) (* btcec.PrivateKey , error ) {
841878
842- // TODO(yy): SQL-only accounts (created via Store.CreateDerivedAccount
843- // without a mirrored legacy waddrmgr account) miss both
844- // DeriveFromKeyPathCache and the DB-backed DeriveFromKeyPath fallback
845- // below because the legacy waddrmgr has no row for them. The
846- // signer-store PR (impl-tx-creator-store) will replace this path for
847- // SQL-only accounts with a keyVault-backed derivation: fetch
848- // account_secrets.encrypted_priv_key, decrypt via w.keyVault, and
849- // derive at branch/index locally — symmetric to deriveAddressData's
850- // AccountPubKey plumbing on the public-key side.
879+ // SQL-only accounts (created via Store.CreateDerivedAccount without a
880+ // mirrored legacy waddrmgr account) miss both DeriveFromKeyPathCache
881+ // and the DB-backed DeriveFromKeyPath fallback below because the legacy
882+ // waddrmgr has no row for them. Each of those misses therefore falls
883+ // through to resolveDerivedPrivKeyFromStore, which fetches
884+ // account_secrets.encrypted_priv_key, decrypts it via w.keyVault, and
885+ // derives at branch/index locally.
851886 accountManager , err := w .addrStore .FetchScopedKeyManager (keyScope )
852887 if err != nil {
888+ if isWaddrmgrAccountClassError (
889+ err , waddrmgr .ErrScopeNotFound , waddrmgr .ErrAccountNotFound ,
890+ ) {
891+
892+ privKey , storeErr := w .resolveDerivedPrivKeyFromStore (
893+ ctx , keyScope , derivationPath ,
894+ )
895+ if storeErr != nil {
896+ return nil , fmt .Errorf ("store account fallback after " +
897+ "legacy scope miss: %w: %w" , err , storeErr )
898+ }
899+
900+ return privKey , nil
901+ }
902+
853903 return nil , fmt .Errorf ("fetch scoped key manager: %w" , err )
854904 }
855905
@@ -861,11 +911,28 @@ func (w *Wallet) resolveDerivedPathPrivKey(keyScope waddrmgr.KeyScope,
861911 // Only a cold account cache warrants the slower DB-backed fallback. Other
862912 // derivation errors are real failures that re-running through the database
863913 // will not repair.
864- if ! waddrmgr . IsError (err , waddrmgr .ErrAccountNotCached ) {
914+ if ! isWaddrmgrAccountClassError (err , waddrmgr .ErrAccountNotCached ) {
865915 return nil , fmt .Errorf ("derive private key from cache: %w" , err )
866916 }
867917
868- return w .resolveDerivedPrivKey (accountManager , derivationPath )
918+ privKey , err = w .resolveDerivedPrivKey (accountManager , derivationPath )
919+ if err == nil {
920+ return privKey , nil
921+ }
922+
923+ if ! isWaddrmgrAccountClassError (err , waddrmgr .ErrAccountNotFound ) {
924+ return nil , err
925+ }
926+
927+ privKey , storeErr := w .resolveDerivedPrivKeyFromStore (
928+ ctx , keyScope , derivationPath ,
929+ )
930+ if storeErr != nil {
931+ return nil , fmt .Errorf ("store account fallback after legacy " +
932+ "account miss: %w: %w" , err , storeErr )
933+ }
934+
935+ return privKey , nil
869936}
870937
871938// resolveDerivedPrivKey resolves one derived private key through the normal
@@ -905,6 +972,44 @@ func (w *Wallet) resolveDerivedPrivKey(accountManager waddrmgr.AccountStore,
905972 return privKey , nil
906973}
907974
975+ // resolveDerivedPrivKeyFromStore resolves one derived private key from the
976+ // account-level encrypted secret stored behind the wallet store.
977+ func (w * Wallet ) resolveDerivedPrivKeyFromStore (ctx context.Context ,
978+ keyScope waddrmgr.KeyScope ,
979+ path waddrmgr.DerivationPath ) (* btcec.PrivateKey , error ) {
980+
981+ if w .cache == nil {
982+ return nil , fmt .Errorf ("%w: cache" , ErrMissingParam )
983+ }
984+
985+ secret , err := w .cache .GetAccountSecret (ctx , db.GetAccountSecretQuery {
986+ WalletID : w .id ,
987+ Scope : db .KeyScope (keyScope ),
988+ AccountNumber : & path .InternalAccount ,
989+ })
990+ switch {
991+ case errors .Is (err , db .ErrAccountSecretUnavailable ),
992+ errors .Is (err , db .ErrAccountNotFound ):
993+
994+ return nil , ErrAccountNotInStore
995+
996+ case err != nil :
997+ return nil , fmt .Errorf ("fetch account secret: %w" , err )
998+ }
999+
1000+ if len (secret .EncryptedPrivateKey ) == 0 {
1001+ return nil , ErrWatchOnlyAccount
1002+ }
1003+
1004+ if w .keyVault == nil {
1005+ return nil , fmt .Errorf ("%w: keyVault" , ErrMissingParam )
1006+ }
1007+
1008+ return deriveStoredAccountChildKey (
1009+ w .keyVault , secret .EncryptedPrivateKey , path ,
1010+ )
1011+ }
1012+
9081013// deriveStoredAccountChildKey decrypts an account's encrypted private key with
9091014// the wallet's keyVault and walks the branch and index derivation to produce
9101015// the leaf private key. The decrypted byte slice and intermediate HD keys are
@@ -1054,26 +1159,18 @@ func redeemSigScript(redeemScript []byte) ([]byte, error) {
10541159
10551160// ComputeRawSig generates a raw signature for a single transaction input. The
10561161// caller is responsible for assembling the final witness.
1057- func (w * Wallet ) ComputeRawSig (_ context.Context , params * RawSigParams ) (
1162+ func (w * Wallet ) ComputeRawSig (ctx context.Context , params * RawSigParams ) (
10581163 RawSignature , error ) {
10591164
10601165 err := w .state .canSign ()
10611166 if err != nil {
10621167 return nil , err
10631168 }
10641169
1065- // Get the managed address for the specified derivation path. This will
1066- // be used to retrieve the private key.
1067- managedAddr , err := w .fetchManagedPubKeyAddress (params .Path )
1170+ privKey , err := w .derivePathPrivKey (ctx , params .Path )
10681171 if err != nil {
10691172 return nil , err
10701173 }
1071-
1072- // Get the private key for the address.
1073- privKey , err := managedAddr .PrivKey ()
1074- if err != nil {
1075- return nil , fmt .Errorf ("cannot get private key: %w" , err )
1076- }
10771174 defer privKey .Zero ()
10781175
10791176 // If a tweaker is provided, we'll use it to tweak the private key.
@@ -1099,25 +1196,15 @@ func (w *Wallet) ComputeRawSig(_ context.Context, params *RawSigParams) (
10991196// path.
11001197//
11011198// DANGER: This method exports sensitive key material.
1102- func (w * Wallet ) DerivePrivKey (_ context.Context , path BIP32Path ) (
1199+ func (w * Wallet ) DerivePrivKey (ctx context.Context , path BIP32Path ) (
11031200 * btcec.PrivateKey , error ) {
11041201
11051202 err := w .state .canSign ()
11061203 if err != nil {
11071204 return nil , err
11081205 }
11091206
1110- managedPubKeyAddr , err := w .fetchManagedPubKeyAddress (path )
1111- if err != nil {
1112- return nil , err
1113- }
1114-
1115- privKey , err := managedPubKeyAddr .PrivKey ()
1116- if err != nil {
1117- return nil , fmt .Errorf ("cannot get private key: %w" , err )
1118- }
1119-
1120- return privKey , nil
1207+ return w .derivePathPrivKey (ctx , path )
11211208}
11221209
11231210// GetPrivKeyForAddress returns the private key for a given address.
@@ -1140,7 +1227,7 @@ func (w *Wallet) GetPrivKeyForAddress(ctx context.Context, a address.Address) (
11401227 info , err := w .GetAddressInfo (ctx , a )
11411228 switch {
11421229 case err == nil && canUseAddressInfoDerivation (info ):
1143- return w .privKeyForAddressInfo (info )
1230+ return w .privKeyForAddressInfo (ctx , info )
11441231
11451232 case err == nil :
11461233 // Store record exists but no usable derivation info
0 commit comments