Skip to content

Commit dc68c3c

Browse files
committed
db: populate AddressInfo.HasDerivationPath
Set HasDerivationPath on the shared and kvdb address-info paths so HD-derived addresses (derived accounts and imported-xpub watch-only children) are distinguished from raw single imports, and accept imported-xpub HD rows in convertAddressPath instead of rejecting them.
1 parent 7133a51 commit dc68c3c

2 files changed

Lines changed: 55 additions & 23 deletions

File tree

wallet/internal/db/addresses_common.go

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -459,23 +459,41 @@ func convertAddressMetadata[TypeID, OriginIDType any](
459459
}
460460

461461
// convertAddressPath converts BIP44 branch/index values into uint32 fields.
462-
// Imported addresses must have both branch/index unset and return zero values.
463-
// Derived addresses must have both fields set and convertible to uint32.
462+
//
463+
// The both-or-neither invariant holds for every origin: a row must carry both
464+
// branch and index or neither, and exactly one set is a corrupt path. Beyond
465+
// that, the meaning of an unset path depends on the origin:
466+
//
467+
// - A derived account is always HD, so both fields must be set.
468+
// - An imported account spans two shapes. A raw/bucket import (a single
469+
// pubkey or script with no chain position) carries neither field and maps
470+
// to zero values. An imported-xpub (watch-only xpub) account is HD: the
471+
// scan-batch horizon extension derives child addresses from its account
472+
// xpub and persists them with a real branch/index, which the schema
473+
// permits (only the both-or-neither and range constraints apply). Those
474+
// rows must read back with their derivation path, not be rejected, so they
475+
// surface through IterAddresses/ListAddresses like any HD address.
464476
func convertAddressPath(origin AccountOrigin, branch,
465477
index sql.NullInt64) (uint32, uint32, error) {
466478

467-
if origin == ImportedAccount {
468-
if branch.Valid || index.Valid {
469-
return 0, 0, errInvalidDerivationPath
470-
}
471-
472-
return 0, 0, nil
479+
// Reject a half-populated path for every origin; the remaining branches
480+
// only have to distinguish "both unset" from "both set".
481+
if branch.Valid != index.Valid {
482+
return 0, 0, errInvalidDerivationPath
473483
}
474484

475-
if !branch.Valid || !index.Valid {
485+
// A raw/bucket imported address carries no chain position and reads back
486+
// as zero values; a derived account must always have a path set.
487+
if !branch.Valid {
488+
if origin == ImportedAccount {
489+
return 0, 0, nil
490+
}
491+
476492
return 0, 0, errInvalidDerivationPath
477493
}
478494

495+
// Both fields are set: a derived address or an imported-xpub HD address,
496+
// converted the same way.
479497
addrBranch, err := Int64ToUint32(branch.Int64)
480498
if err != nil {
481499
return 0, 0, fmt.Errorf("address branch: %w", err)
@@ -520,6 +538,11 @@ func AddressRowToInfo[TypeID, OriginIDType any](
520538
return nil, err
521539
}
522540

541+
// A row carries an HD path iff both nullable path columns are present.
542+
// convertAddressPath has already rejected a half-populated path, so the
543+
// two Valid flags agree here; checking both keeps the intent explicit.
544+
hasDerivationPath := row.AddressBranch.Valid && row.AddressIndex.Valid
545+
523546
return &AddressInfo{
524547
ID: id,
525548
AccountID: accountID,
@@ -532,6 +555,7 @@ func AddressRowToInfo[TypeID, OriginIDType any](
532555
Origin: origin,
533556
Branch: addrBranch,
534557
Index: addrIndex,
558+
HasDerivationPath: hasDerivationPath,
535559
ScriptPubKey: row.ScriptPubKey,
536560
PubKey: row.PubKey,
537561
HasScript: row.HasScript,
@@ -761,16 +785,17 @@ func createDerivedAddress[T any](ctx context.Context,
761785
}
762786

763787
return &AddressInfo{
764-
ID: id,
765-
AccountID: convertedAcctID,
766-
AddrType: addrType,
767-
CreatedAt: rowCreatedAt(row),
768-
Origin: DerivedAccount,
769-
Branch: branch,
770-
Index: index,
771-
ScriptPubKey: scriptPubKey,
772-
PubKey: pubKey,
773-
IsWatchOnly: walletIsWatchOnly,
788+
ID: id,
789+
AccountID: convertedAcctID,
790+
AddrType: addrType,
791+
CreatedAt: rowCreatedAt(row),
792+
Origin: DerivedAccount,
793+
Branch: branch,
794+
Index: index,
795+
HasDerivationPath: true,
796+
ScriptPubKey: scriptPubKey,
797+
PubKey: pubKey,
798+
IsWatchOnly: walletIsWatchOnly,
774799
}, nil
775800
}
776801

wallet/internal/db/kvdb/addressstore.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -851,23 +851,29 @@ func managedAddressInfo(ns walletdb.ReadBucket,
851851
}
852852

853853
var (
854-
branch uint32
855-
index uint32
856-
fingerprint uint32
857-
pubKey []byte
854+
branch uint32
855+
index uint32
856+
fingerprint uint32
857+
pubKey []byte
858+
hasDerivationPath bool
858859
)
859860

860861
pubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
861862
if ok {
862863
pubKey = managedAddressPubKey(pubKeyAddr)
863864

865+
// DerivationInfo reports ok only for HD-derived addresses (both
866+
// normal derived accounts and imported-xpub watch-only children);
867+
// raw single imports return false because their key has no known
868+
// chain position. That is exactly the HasDerivationPath signal.
864869
scope, path, ok := pubKeyAddr.DerivationInfo()
865870
if ok {
866871
accountNumber = path.InternalAccount
867872
branch = path.Branch
868873
index = path.Index
869874
fingerprint = path.MasterKeyFingerprint
870875
keyScope = db.KeyScope(scope)
876+
hasDerivationPath = true
871877
}
872878
}
873879

@@ -888,6 +894,7 @@ func managedAddressInfo(ns walletdb.ReadBucket,
888894
Origin: origin,
889895
Branch: branch,
890896
Index: index,
897+
HasDerivationPath: hasDerivationPath,
891898
ScriptPubKey: scriptPubKey,
892899
PubKey: pubKey,
893900
HasScript: addrType.HasScript,

0 commit comments

Comments
 (0)