Skip to content

Commit c5c3305

Browse files
committed
wallet: route signer keys through Store on legacy miss
Add derivePathPrivKey and resolveDerivedPrivKeyFromStore, and thread a context through the signing paths (ECDH, SignDigest, ComputeRawSig, DerivePrivKey and the output/imported-key helpers). When the legacy waddrmgr lookup misses on a scope or account, resolve the account's encrypted private key from the Store and derive the leaf key through the key vault, with precise watch-only and not-in-store errors. Covers SQL-only accounts that have no mirrored waddrmgr row. (cherry picked from commit de9ddb6)
1 parent 9094814 commit c5c3305

2 files changed

Lines changed: 196 additions & 66 deletions

File tree

wallet/signer.go

Lines changed: 147 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)