Skip to content

Use BIP-44 change chain (internal chain) for change addresses, not external+1 #2056

@BullishNode

Description

@BullishNode

WalletAddressRepository.generateNewReceiveAddress(...) is currently used as the source of change addresses across the codebase. For LWK wallets it derives from the external receive chain at lastUnusedIndex + 1:

// lib/core/wallet/data/repositories/wallet_address_repository.dart:91-122
final lastUnusedAddressInfo = await _lwkWallet.getLastUnusedAddress(...);
final addressInfo = await _lwkWallet.getAddressByIndex(
  lastUnusedAddressInfo.index + 1,
  wallet: walletModel,
);

So change outputs land at m/84'/<coin_type>'/<account>'/0/<i+1> instead of the BIP-44 standard m/.../1/<j>.

Why this matters

  • Privacy. Change and inbound payments cluster on a single derivation lineage. Chain-analysis tooling that assumes BIP-44 chain separation can correlate self-spends with receives more easily than it should.
  • Gap-limit pressure. External-chain indices are consumed at roughly double the natural rate. Default gap-limit restores (20) can miss balance after a relatively small number of back-to-back self-spends.
  • BIP-44 non-conformance. A restored seed in any other compliant wallet will not classify these outputs as change — they'll appear as additional external receives.

Not a fund-safety issue; funds at m/.../0/N+1 are always recoverable. The cost is privacy and interoperability.

Why the workaround exists

The Dart Wallet API exposed by lwk-dart currently has no change-chain primitive — only external-chain address(index:) and addressLastUnused() are surfaced. Tracking issue: SatoshiPortal/lwk-dart#70.

Proposed change

Blocked on lwk-dart exposing changeAddressLastUnused() / changeAddress(index:). Once those land:

  1. Add getChangeAddressByIndex(...) and getLastUnusedChangeAddress(...) to LwkWalletDatasource, calling the new lwk-dart methods.
  2. Add generateNewChangeAddress({required String walletId}) and getLastUnusedChangeAddress({required String walletId}) to WalletAddressRepository, mirroring the existing receive-address methods but routing to the change chain.
  3. Migrate any call site that currently uses generateNewReceiveAddress as a change address to the new change-specific methods.
  4. For BDK wallets the equivalent primitives (peek_change_address / next_change_address) are already in bdk_flutter; thread those through the same repository methods so behavior is uniform across BDK and LWK wallets.

Test plan

  • Generated change addresses derive at m/.../1/<i> (verified via descriptor inspection).
  • Existing transaction-building flows still pass after migration.
  • Restore-from-seed scenario: wallet restored in an external BIP-44 tool sees external receives on chain 0 and change on chain 1, with tx history correctly classified.
  • Existing integration tests covering wallet-creates-transaction flows continue to pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions