Skip to content

DIP-15 contact address pools never extend — the 21st payment from any contact is permanently invisible #1032

Description

@HashEngineering

Impact

A DashPay contact chain watches exactly 20 addresses, indices 0–19, and never grows. Once a contact has sent 20 payments, every later payment lands on an address the receiving wallet never derived, so it is never matched by the BIP158 scan and never enters the balance. A rescan cannot recover it, because the address does not exist client-side. Coins are on-chain and spendable by the seed but invisible to the wallet.

Also affects DashpayExternalAccount, so the wallet stops tracking its own outgoing contact payments past index 19.

Reproduction (testnet, confirmed)

Twenty sequential contact payments filled indices 0–19 and were all received. The 21st was not:

txid   b837bcb2f4f50c29502cd32549b3c369f28157b4ead872b7f41351cebb121ab0
value  0.00029000 DASH -> yR3kpYnbcPipq4982WcEK2mCBrQXh7P39K
block  1555216

Receiver synced through block 1555217, service running: zero transactions rows, zero txos rows, and zero core_addresses rows for that destination. The address was never derived. Highest known index 19, all 20 used, unspent total unchanged.

The window never moved across all 20 uses, on all 14 of that wallet's contact accounts: at highest_used 19 it was still 0–19, where highest_used + gap_limit would be 0–39.

The sender's log shows broadcastTransaction … complete and the explorer confirms the transaction, so it was sent. The receiver had just caught 19 consecutive payments on the same chain, so it was not stalled.

Root cause

Verified on master e5423d2a, and on af88edf where it was reproduced.

key-wallet/src/transaction_checking/wallet_checker.rs:262-291 marks addresses used unconditionally, then maintains the gap limit only if an xpub resolves:

for address_info in account_match.account_type_match.all_involved_addresses() {
    account.mark_address_used(&address_info.address);
}

let account_type_to_check = account_match.account_type_match.to_account_type_to_check();
let xpub_opt = wallet.extended_public_key_for_account_type(
    &account_type_to_check,
    account_match.account_type_match.account_index(),
);

if let Some(xpub) = xpub_opt {
    ...
    let _ = pool.maintain_gap_limit(&key_source);
}

For DashPay accounts xpub_opt is always None, so maintain_gap_limit never runs and the pool keeps its construction size. AddressPool does not retain a key source, so it cannot extend itself.

The None comes from key-wallet/src/wallet/helper.rs:770:

AccountTypeToCheck::DashpayReceivingFunds |
AccountTypeToCheck::DashpayExternalAccount => {
    // Currently not retrieved via this helper
    None
}

This is correct as written. AccountTypeToCheck is a fieldless enum, so its DashPay variants carry no index, user_identity_id or friend_identity_id, and contact accounts are keyed by all three. The helper cannot identify which contact account is meant. The bug is that the caller relies on a lookup that structurally cannot answer for these types.

Because mark_address_used runs above the check, used flags advance perfectly while the window stays frozen. The account looks healthy and fully consumed in the data, which is why this is easy to miss.

Both pools are also constructed with a bare literal 20 rather than a named constant, at managed_account_type.rs:645 and :667.

Suggested fix

  • In wallet_checker.rs, resolve the key source from the account's fully-keyed AccountType rather than the lossy AccountTypeToCheck. The call site already has it via to_account_type(), account_of_type() in account_collection.rs already handles both DashPay variants, and Account.account_xpub is public. DIP-15 receiving addresses derive from that xpub alone, so no seed or private key is needed.
  • Replace the hardcoded 20 with a named constant. Note DEFAULT_CONTACT_GAP_LIMIT = 10 exists in dashpay/platform at rs-platform-wallet/src/wallet/identity/crypto/dip14.rs:254, quoting DIP-15, but has no consumer anywhere. Decide which value is intended.
  • Consider whether mark_address_used should run when the pool cannot be maintained, since that mismatch is what hides the failure.

Related

dashpay/platform PR #4740 is adjacent but does not fix this. It registers contact accounts with the scanner and rewinds to the request height; it touches no gap-limit or address-pool code. A wallet with it merged still loses payment 21.

Environment

dashpay/rust-dashcore master e5423d2a (verified) and af88edf (reproduced), via dashpay/platform rs-platform-wallet. Dash Platform SDK 4.2.0-dev.8, Android wallet 12.0.0, Dash testnet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions