Skip to content

[Bug]: RBF boost home list sometimes drops BoostingIcon and shows the replaced send #1317

Description

@piotr-iohk

What happened?

After a successful on-chain RBF boost, the home activity list is nondeterministic.

Expected (linking sticks): one pending boosted send (yellow timer / BoostingIcon, "Confirms in ±Xm", -10 422) and the +100 000 receive. The original send is hidden.

Race (linking lost): the replacement is a normal pending send (orange arrow, "Confirms in ±10m", no boost). The original stays visible as "Removed from Mempool". Receive is unchanged. Same amounts (-10 422). Balance is correct (89 578 = 100000 − 10422).

On-chain RBF is fine (fee 141 → 422 sat). Only activity metadata is wrong: isBoosted / boostTxIds on the replacement. Without boostTxIds, filterOutReplacedSentTransactions cannot hide the original. Without isBoosted, isBoosting() is false so BoostingIcon never appears.

Local repro 2026-09-21 10:11 (UTC+2) / log bitkit_2026-09-21_08-09-33.log: original 05b61da4…, replacement b21eb6e9…. Same UI seen earlier on master and on #1296 (seed paste — unrelated).

Expected behavior

  • Home list after RBF: pending replacement with BoostingIcon, original hidden.
  • Detail of the replacement: boosted / confirming, not a plain send.
  • @boost_2 (BoostingIcon after BoostSuccessToast) is the correct assertion. Do not change e2e to accept the three-row list.

Steps to Reproduce

  1. New wallet on local / regtest Electrum (e2e or manual).
  2. Receive 100 000 on-chain and confirm it.
  3. Send 10 000 on-chain; leave it unconfirmed.
  4. Open that send → Boost → confirm RBF.
  5. After the boost success toast, look at the home activity list.
  6. Repeat. Sometimes the yellow timer + one send; sometimes orange arrow + "Removed from Mempool".

@boost_2 in bitkit-e2e-tests test/specs/boost.e2e.ts is this path. @boost_1 is CPFP and does not hit replace/evict.

Logs / Screenshots / Recordings

Expected (linking stuck):

home list after RBF: yellow BoostingIcon, original hidden

Race (link overwritten) — 10:11 local, matches fail log 08-09-33:

home list after RBF: plain pending send, Removed from Mempool, receive

Logs:

Fail key lines (UTC), original 05b61da4… → replacement b21eb6e9…:

08:11:03.858  RBF transaction created successfully!
              Original: 05b61da4… (141 sats)  Replacement: b21eb6e9… (422 sats)
08:11:04.063  Boost successful. newTxId: b21eb6e9…
08:11:04.193  Synced 2 payments   ← replacement not in the list yet
08:11:04.624  OnchainTransactionReceived b21eb6e9…  (−10422)
              05b61da4… was replaced by 1 transaction(s)
08:11:04.695  OnchainTransactionReplaced 05b61da4… → [b21eb6e9…]
08:11:04.917  Marked transaction 05b61da4… as replaced
08:11:05.012  Processing replacement b21eb6e9… that was already in payments list
              (no activity row yet → created as a plain send)
08:11:05.064  Updated replacement b21eb6e9… with boostTxId 05b61da4…
08:11:06.138  Synced 3 payments   ← after the boost link; overlapping writer still running

Bitkit Version

2.4.1 (188). Reproduced on master and on #1296. Not introduced by #1296.

Device / OS

Android emulator (Pixel), local docker Electrum / BACKEND=local e2e.

Reproducibility

Sometimes (<50%)

Additional context

  • New wallet, on-chain only. No Lightning channel.
  • BoostTransactionViewModel.handleRBFUpdate only marks the original isBoosted=true, then syncAsync(). The replacement is linked later in CoreService.handleOnchainTransactionReplaced (doesExist=false on the original; isBoosted=true + original txid in boostTxIds on the replacement).
  • Both fail (08-09-33) and pass (08-24-45) hit Processing replacement … already in payments list then Updated replacement … with boostTxId. The pass then cancelled a competing syncActivities (JobCancellationException) and a later Synced 3 payments kept the link. The fail’s Synced 3 payments after the link did not. Same events; metadata does not always survive.
  • Home filter: Activity.isReplacedSentTransaction requires !doesExist && SENT && txId in boostTxIds. Icon: isBoosting() = isBoosted && !confirmed && doesExist.
  • iOS: same handleOnchainTransactionReplaced / isReplacedSentTransaction / boosting icon. Not reproduced on iOS in this pass.
  • Closed the mistaken e2e change that treated the race UI as product: bitkit-e2e-tests#241.
  • Fix proposal (not a patch): processOnchainPayment / buildNewOnchainActivity can upsert a blank replacement (isBoosted=false) after handleOnchainTransactionReplaced has already stamped boostTxIds. ServiceQueue.CORE reenters when a background {} suspends; Received / Replaced / syncActivities overlap. Logs do not name the last unboosted write.
    1. Never clobber existing isBoosted, boostTxIds, or doesExist=false on an on-chain upsert (this alone makes the race harmless).
    2. Create the replacement already boosted in handleOnchainTransactionReplaced, instead of blank insert then stamp.
    3. Serialize on-chain LDK activity events and sync payments sequentially (iOS already sequential).

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions