Skip to content
Merged
Show file tree
Hide file tree
Changes from 181 commits
Commits
Show all changes
185 commits
Select commit Hold shift + click to select a range
b994bb6
fix: restart the SDK L1 engine on every service start, not once per p…
HashEngineering Aug 28, 2026
ce0bc15
fix: crash tapping the fiat option on the Coinbase transfer screen (M…
HashEngineering Aug 28, 2026
5cbffa8
fix: never commit the cutover onto an SDK that cannot own L1 (MO-995)
HashEngineering Aug 31, 2026
9379540
fix: persist the bind-success marker on every successful bind, not ju…
HashEngineering Aug 31, 2026
4606b31
test: fix three more emulator-harness bugs that faked results (MO-995)
HashEngineering Aug 31, 2026
2f574b8
fix: refuse EVERY path into CUT_OVER without SDK bind evidence (MO-995)
HashEngineering Aug 31, 2026
cad574c
test: make the emulator harness able to unlock the keyguard (MO-995)
HashEngineering Aug 31, 2026
1b25d6e
fix: make an app-foreground visit actually retry the SDK bind (MO-995)
HashEngineering Aug 31, 2026
984ca67
fix: get the app-foreground signal from a hook that actually fires (M…
HashEngineering Aug 31, 2026
5d89950
fix: take the foreground signal from the counter that actually runs (…
HashEngineering Aug 31, 2026
05eee34
test: assert what S3 can actually observe, and split the commit check…
HashEngineering Aug 31, 2026
71819dd
test: take the emulator PIN from EMULATOR_PIN, never hardcode it (MO-…
HashEngineering Aug 31, 2026
091c2ba
fix: tell auto-logout the app went to the background again (MO-995)
HashEngineering Sep 1, 2026
e4993d2
fix: never announce our own send as a receive (MO-995)
HashEngineering Sep 1, 2026
c41b7a1
diag: log what the CoinJoin classifier returns, and stop it crashing …
HashEngineering Sep 1, 2026
ad66c67
fix: stop minifying the uphold library — it strips Hilt's aggregated …
HashEngineering Sep 1, 2026
fba24cb
fix: Block filters no longer reads "-" on a synced wallet (MO-995)
HashEngineering Sep 2, 2026
cdb17f3
fix: don't show the "happens only once" sync notice twice (MO-995)
HashEngineering Sep 2, 2026
ea513d4
diag: say how long the SDK engine's loops were torn down (MO-995)
HashEngineering Sep 2, 2026
fddb0e2
fix: emulator harness exits non-zero on FAIL, and has one boundary (M…
HashEngineering Sep 2, 2026
21748de
fix: let a fresh wallet commit the cutover again — my regression (MO-…
HashEngineering Sep 3, 2026
5d5e59d
fix: satisfy the foreground-service promise synchronously (MO-995)
HashEngineering Sep 3, 2026
497dec7
fix: arm the one-time sync explainer from whichever path commits (MO-…
HashEngineering Sep 4, 2026
4f74977
fix: block filters read "-" for a minute after every cold start (MO-1…
HashEngineering Sep 4, 2026
0b31e79
fix: stop tearing down L1 every time the user leaves the app (MO-995)
HashEngineering Sep 6, 2026
accbd29
fix: keep grpc-netty-shaded Epoll class for R8 release builds
HashEngineering Sep 8, 2026
e9f178e
fix: keep the whole shaded netty transport from R8 (MO-973)
HashEngineering Sep 8, 2026
962d4de
fix: an advisory health probe must not be able to kill the app (MO-973)
HashEngineering Sep 8, 2026
eebf0be
diag: say why username creation failed instead of guessing (MO-973, M…
HashEngineering Sep 10, 2026
82969cc
fix: don't lose the upgrade explainer to a dropped latch write (MO-1022)
HashEngineering Sep 11, 2026
783c065
walker: guard reattribution against an incomplete TXO mirror
HashEngineering Aug 20, 2026
1266edc
fix: run the SDK's ordered DashPay bring-up before SPV, and retire th…
HashEngineering Sep 8, 2026
0b62d0e
fix(dashpay): stop re-registering a contested name the identity alrea…
HashEngineering Sep 10, 2026
00607c9
fix(dashpay): take dashj-platform 4.0.1 for the identity CBOR crash (…
HashEngineering Sep 11, 2026
b89475b
fix: three live-QA bugs from MO-973 report 3 (Samsung, 12000011)
HashEngineering Sep 11, 2026
f72633b
fix(dashpay): show the completion card when a contested name wins its…
HashEngineering Sep 11, 2026
080da8e
fix: address CodeRabbit review on #1564 — stage seeding and the colli…
HashEngineering Sep 12, 2026
6dfdc56
test: prove the caught-up blip actually ran before asserting the stag…
HashEngineering Sep 12, 2026
3ab8655
fix(dashpay): stop the confirm dialog hijacking the instant-username …
HashEngineering Sep 12, 2026
291cecf
fix(dashpay): raise the confirm dialog from user action only, not obs…
HashEngineering Sep 12, 2026
517f201
docs: say why the explainer latch is split from its eligibility tests
HashEngineering Sep 14, 2026
39d188d
test: S4 asserted on log lines this PR had already renamed (MO-995)
HashEngineering Sep 15, 2026
45c83c8
test: a late level-40 teardown could pass S4's COMPLETE assertion (MO…
HashEngineering Sep 15, 2026
21ce826
fix: my boundary-latch retry sat on a path the upgrade launch never t…
HashEngineering Sep 15, 2026
215fd66
docs: reattach three KDoc blocks this PR had separated from their mem…
HashEngineering Sep 15, 2026
9895ec5
docs: v12 upgrade memory and sync recovery plan
HashEngineering Sep 16, 2026
c77b5e8
fix: rotating a screen must not lock the wallet (MO-995)
HashEngineering Sep 16, 2026
828538c
walker: guard reattribution against an incomplete TXO mirror
HashEngineering Aug 20, 2026
a190506
fix: run the SDK's ordered DashPay bring-up before SPV, and retire th…
HashEngineering Sep 8, 2026
9b62c7b
build: consume the published SDK snapshot 0.1.0-v42int19-SNAPSHOT
HashEngineering Sep 15, 2026
f5bb9f4
walker: scope the tracked-address probe to this wallet
HashEngineering Sep 15, 2026
4f0ea77
test: pin the ordered DashPay bring-up contract in L1ShadowSyncServic…
HashEngineering Sep 15, 2026
be54972
walker: judge mirror completeness from the same read as the funded sums
HashEngineering Sep 16, 2026
c3948c7
test: prove SPV waits for a suspended bring-up, and that cancellation…
HashEngineering Sep 16, 2026
3d40f83
fix(dashpay): stop re-registering a contested name the identity alrea…
HashEngineering Sep 10, 2026
8b62b0f
fix(dashpay): take dashj-platform 4.0.1 for the identity CBOR crash (…
HashEngineering Sep 11, 2026
36defff
fix: three live-QA bugs from MO-973 report 3 (Samsung, 12000011)
HashEngineering Sep 11, 2026
cd8f39a
fix(dashpay): show the completion card when a contested name wins its…
HashEngineering Sep 11, 2026
fb6f516
fix: address CodeRabbit review on #1564 — stage seeding and the colli…
HashEngineering Sep 12, 2026
b85ada3
test: prove the caught-up blip actually ran before asserting the stag…
HashEngineering Sep 12, 2026
ed7e45e
fix(dashpay): stop the confirm dialog hijacking the instant-username …
HashEngineering Sep 12, 2026
f17003b
fix(dashpay): raise the confirm dialog from user action only, not obs…
HashEngineering Sep 12, 2026
ae26027
docs: add the 2026-09-16 emulator upgrade test to the memory and sync…
HashEngineering Sep 16, 2026
99969f5
docs: reference install follow-up after the user-initiated rescan
HashEngineering Sep 16, 2026
d9d6031
docs: revise the plan for the immediate, no-fallback cutover policy
HashEngineering Sep 16, 2026
d4d8dd6
build: consume the published SDK snapshot 0.1.0-v42int19-SNAPSHOT
HashEngineering Sep 15, 2026
aeb9769
fix(cutover): commit on the upgrade launch and never start dashj on i…
HashEngineering Sep 16, 2026
96811ae
fix(cutover): remove the dashj fallback and the automatic SDK wallet …
HashEngineering Sep 16, 2026
1ba7f81
feat(cutover): "SDK setup pending" state for a blocked SDK wallet bind
HashEngineering Sep 16, 2026
b7c588b
fix(cutover): defer the first SDK bind while the device is provably l…
HashEngineering Sep 16, 2026
790ae16
fix(logging): quiet dashj's InstantSend/quorum chatter and per-tx met…
HashEngineering Sep 16, 2026
bb02700
fix(wallet): size-aware autosave debounce for large wallet files
HashEngineering Sep 16, 2026
2097531
fix(sync): keep the blockchain service alive while a replay is in pro…
HashEngineering Sep 16, 2026
20dc062
fix(sync): mark the replay as started when the cutover hands an upgra…
HashEngineering Sep 16, 2026
b42f8f8
fix(sync): arm the one-minute restart when a replay is interrupted by…
HashEngineering Sep 16, 2026
10ba754
fix(sync): bound the DashPay bring-up wait so SPV starts within 20 s
HashEngineering Sep 16, 2026
8ef28d1
fix(sync): wait for the previous service instance before opening the …
HashEngineering Sep 16, 2026
4b8f50e
fix(sync): do not start the SDK engine into a blockchain service that…
HashEngineering Sep 16, 2026
839f0d6
fix(sync): start the SDK L1 engine only from the foreground blockchai…
HashEngineering Sep 16, 2026
40513a6
fix(sync): back the 15 s platform ticker off to five minutes in the b…
HashEngineering Sep 16, 2026
d25c784
diag(sync): log the SDK watermark gap at every engine stop
HashEngineering Sep 16, 2026
962bf8c
docs: record the Phase 1a/1b implementation and where it departs from…
HashEngineering Sep 16, 2026
326fc31
fix(upgrade): start syncing after a package replacement, and stop los…
HashEngineering Sep 16, 2026
7291395
docs: record the 2026-09-16 two-emulator upgrade test and its three d…
HashEngineering Sep 16, 2026
3b697a3
fix(cutover): export the unlock-heal receiver, or SystemUI's broadcas…
HashEngineering Sep 16, 2026
0596e71
docs: record the second emulator round, the receiver-export defect, a…
HashEngineering Sep 16, 2026
d9b27c7
docs: add the locked-device contact-payment test
HashEngineering Sep 16, 2026
28562fe
docs: withdraw the redundant half of the mid-sync DashPay item
HashEngineering Sep 16, 2026
ae0761a
docs(kill-list): app-side DIP-15 friend-chain provisioning is now red…
HashEngineering Sep 16, 2026
9261430
docs: generalise the uncovered-contact-chain defect beyond the locked…
HashEngineering Sep 16, 2026
e0df0ed
diag(dashpay): report contact chains registered below the scan height
HashEngineering Sep 16, 2026
0e11d3c
docs: disprove section 17's premise by experiment; withdraw the 17.4 fix
HashEngineering Sep 17, 2026
1695b83
docs: when a contact payment becomes visible after a locked window
HashEngineering Sep 17, 2026
9ebcbfc
fix(sdk-bind): clear the durable bind blocker when a fresh process binds
HashEngineering Sep 17, 2026
f82c3e9
docs: record round 3 — unlock receiver verified, stale blocker defect…
HashEngineering Sep 17, 2026
71c65e4
docs: section 19 - two incoming contacts on B have no receiving chain
HashEngineering Sep 17, 2026
bdd03dd
docs: correct 19.3 - contact gap limit is 10, not 1000; dashj held af…
HashEngineering Sep 17, 2026
e805c90
docs: section 20 - root cause, DIP-15 contact chains capped at 20 add…
HashEngineering Sep 17, 2026
ab77387
docs: file SDK issue 16 - DIP-15 contact pools never grow past 20 add…
HashEngineering Sep 17, 2026
7c61745
docs: confirm the 20-address contact cap end to end on testnet
HashEngineering Sep 17, 2026
c1dcc3f
docs: link the filed issue dashpay/rust-dashcore#1032
HashEngineering Sep 17, 2026
5686523
docs: section 21 - post-cutover DashPay dark via rotated-out quorums
HashEngineering Sep 17, 2026
406aeaa
fix(dashpay): coverage diagnostic must count SENT contact requests
HashEngineering Sep 17, 2026
10d016e
docs: section 23 - what Phase 1 does for a clean install + restore
HashEngineering Sep 17, 2026
1703b75
docs: section 24 - test 1 run, clean install + restore
HashEngineering Sep 17, 2026
17cf915
fix(boot): delayed service start must bypass the process-importance g…
HashEngineering Sep 17, 2026
b85cf2f
docs: section 26 - contact requests on the restored wallet confirm 23.4
HashEngineering Sep 17, 2026
ead5ede
docs(kill-list): add 1a - balance publisher emits partial rescan state
HashEngineering Sep 17, 2026
5a3d1a3
docs: section 27 - the restore bug loses real money, confirmed
HashEngineering Sep 17, 2026
f8d5c50
docs(kill-list): add 1b - dead dashj broadcast handler logs a false w…
HashEngineering Sep 17, 2026
fd2a317
fix(boot): stop scheduling a post-boot start that cannot succeed
HashEngineering Sep 17, 2026
1a1594f
docs: section 28 - periodic alarm path is conditional on a battery ex…
HashEngineering Sep 17, 2026
72c9cbf
diag(alarm): log whether a background alarm start is permitted
HashEngineering Sep 17, 2026
d6f6ca2
docs: section 29 - locked new-contact test answers section 16
HashEngineering Sep 17, 2026
0b1d8d3
docs: section 30 - consolidated register of the 16-17 September findings
HashEngineering Sep 17, 2026
06f8122
docs: section 31 - what 12000012 does and does not do for the referen…
HashEngineering Sep 17, 2026
5fda01e
merge: bring in fix/contested-username-restore-identity
HashEngineering Sep 17, 2026
55f3cff
docs: section 32 - the periodic alarm is never delivered, not refused
HashEngineering Sep 17, 2026
586ee13
docs: section 32.5-32.6 - forced overdue, and closing the app makes i…
HashEngineering Sep 17, 2026
616ac58
fix(sync): restart the SPV engine when the filter cursor wedges (MO-1…
HashEngineering Sep 18, 2026
acffa2a
docs(plan): section 33 — move the protobuf parse off the main thread
HashEngineering Sep 18, 2026
f4f7852
fix(dashpay): judge §17 contact coverage by registration ordering, no…
HashEngineering Sep 19, 2026
906a1b6
docs(plan): rewrite section 33 — the unit is the wallet LOAD, and pos…
HashEngineering Sep 19, 2026
2083334
docs(plan): fix section 33's intro cross-references
HashEngineering Sep 19, 2026
68676e0
fix(startup): stop a degraded launch crashing in onCreate — setConten…
HashEngineering Sep 19, 2026
c9a766c
fix(dashpay): stop re-registering a contested name the identity alrea…
HashEngineering Sep 10, 2026
2ed4f6a
fix(dashpay): take dashj-platform 4.0.1 for the identity CBOR crash (…
HashEngineering Sep 11, 2026
4988588
fix: three live-QA bugs from MO-973 report 3 (Samsung, 12000011)
HashEngineering Sep 11, 2026
2996615
fix(dashpay): show the completion card when a contested name wins its…
HashEngineering Sep 11, 2026
368007a
fix: address CodeRabbit review on #1564 — stage seeding and the colli…
HashEngineering Sep 12, 2026
291beac
test: prove the caught-up blip actually ran before asserting the stag…
HashEngineering Sep 12, 2026
28e740f
fix(dashpay): stop the confirm dialog hijacking the instant-username …
HashEngineering Sep 12, 2026
b7e09a9
fix(dashpay): raise the confirm dialog from user action only, not obs…
HashEngineering Sep 12, 2026
43b7a4c
docs(plan): section 34 — a database-lock lead on the filter stall
HashEngineering Sep 19, 2026
2a82476
fix(sync): the filter-stall watchdog cancelled its own restart, and f…
HashEngineering Sep 19, 2026
2d174cf
fix(dashpay): bind an availability verdict to the name still in the f…
HashEngineering Sep 19, 2026
0b6cf87
WIP(build): int21 AAR pin + 12000014/12.0.0-upgrade build identity
HashEngineering Sep 19, 2026
1d9bbf6
Merge fix/contested-username-restore-identity — picks up #1555 and #1559
HashEngineering Sep 19, 2026
99d6a1a
docs(plan): record why no-fallback won over #1555's gated cutover (se…
HashEngineering Sep 19, 2026
5058e91
feat(support): attach this process's own logcat to the report bundle
HashEngineering Sep 19, 2026
955a771
docs: QA int19 (Pasta) defect status against this branch
HashEngineering Sep 19, 2026
c3cd29e
docs(plan): correct section 34 — the event stream stopped during the …
HashEngineering Sep 19, 2026
9afd5e7
fix(sync): decide the filter-stall wait by what a restart costs, not …
HashEngineering Sep 19, 2026
5d806c3
fix(sync): back the filter-stall watchdog off between attempts
HashEngineering Sep 19, 2026
90851e8
fix(sync): deliver the periodic background alarm (SR-06) — honour the…
HashEngineering Sep 19, 2026
dabcf45
docs(plan): section 32 — SR-06 implemented, and why the deferral was …
HashEngineering Sep 19, 2026
41089d7
docs(plan): correct the 32.10 recipe, and record that the replay guar…
HashEngineering Sep 19, 2026
5f8c0a8
fix(sync): withdraw the fast near-tip watchdog rung — it judged cheap…
HashEngineering Sep 19, 2026
9281ffe
docs(plan): rewrite section 34 — the stall is a filter batch waiting …
HashEngineering Sep 19, 2026
ade385b
docs(plan): section 32 — the alarm was delivered, and section 28 is a…
HashEngineering Sep 19, 2026
c3c0c79
docs: root-cause D-041 to the SDK's native ledger, and record that it…
HashEngineering Sep 19, 2026
b8f79da
docs: correct the SR-01 stall note and withdraw the "successor batch"…
HashEngineering Sep 21, 2026
a2c1a04
fix(service): stop the duplicate onDestroy from tearing down the real…
HashEngineering Sep 21, 2026
a01c4a7
merge: rebase the stack onto feat/kotlin-sdk-phase-1
HashEngineering Sep 21, 2026
40f60cf
fix(dashpay): write the contact-registration pair in one transaction
HashEngineering Sep 21, 2026
3da0b9b
fix(cutover): the five remaining review suggestions on #1568
HashEngineering Sep 21, 2026
3e3e75c
fix: the three remaining CodeRabbit findings on #1568
HashEngineering Sep 21, 2026
70569d6
fix(sdk): serialize the bind outcome handlers with a mutex
HashEngineering Sep 21, 2026
743d7af
fix(sync): stop the watchdog restarting into the section 34 stall
HashEngineering Sep 21, 2026
1798e6d
docs(plan): section 34 — the withheld commit persists, and the bounda…
HashEngineering Sep 21, 2026
68d2ff0
fix(sync): adopt iOS's sync-status rule; log the final-batch stall in…
HashEngineering Sep 21, 2026
3e9fa5d
fix(sync): log the l1Synced, pipeline-lag and seed-write edges once each
HashEngineering Sep 21, 2026
722e6ea
chore: use dashj 22.0.5 and dpp 4.0.1
HashEngineering Sep 22, 2026
19fdf96
chore: bump version code to build 17
HashEngineering Sep 22, 2026
4a0a9ea
fix: bound the final-batch restart hold, validate bind failures by id…
HashEngineering Sep 22, 2026
0490b82
docs: record the 2026-09-21 device evidence — the iOS sync rule prove…
HashEngineering Sep 22, 2026
bba2be7
docs(plan): section 36.3 — the foreground service demotes itself on a…
HashEngineering Sep 22, 2026
cddf34e
docs(plan): section 35.4 — the display can read HIGH for 1.5 s during…
HashEngineering Sep 22, 2026
da5bfb4
Merge origin/feat/kotlin-sdk-phase-1 into fix/upgrade-memory-and-sync
HashEngineering Sep 22, 2026
205803e
fix(bind-retry): a stale bind success must not clear a newer failure'…
HashEngineering Sep 23, 2026
b817a59
fix(sync-state): derive the replay flag from the engine's lifecycle, …
HashEngineering Sep 23, 2026
8b339a1
fix(l1-shadow): a filter-stall restart must not outlive an external s…
HashEngineering Sep 23, 2026
269bf9e
fix(platform-sync): start the L1 engine when the bind heals inside a …
HashEngineering Sep 23, 2026
02e741b
fix(service): release the replay's own wake-lock acquisition regardle…
HashEngineering Sep 23, 2026
3a09756
fix(sync-state): only dashj's scan-progress writer may end a replay a…
HashEngineering Sep 23, 2026
bbef7a6
fix(l1-shadow): validate the restart's stop generation under the star…
HashEngineering Sep 23, 2026
96029b6
fix(service): release the replay's wake-lock acquisition on teardown too
HashEngineering Sep 23, 2026
1d940f6
fix(l1-shadow): a cancelled bring-up still inside its native call is …
HashEngineering Sep 24, 2026
8821495
fix(bind-retry): a failure that lost the race to a success republishe…
HashEngineering Sep 24, 2026
1f845ff
fix(platform-sync): a startup kick must not outlive the service lifet…
HashEngineering Sep 24, 2026
459a253
test(sync-state): wait for the stage the parked update publishes, not…
HashEngineering Sep 24, 2026
65e304d
fix(service): retire the replay restart alarm — on wipe, and at the n…
HashEngineering Sep 25, 2026
bd31b08
fix(l1-shadow): a destructive step waits for a cancelled bring-up to …
HashEngineering Sep 25, 2026
118a2ac
fix(sdk-bind): decide and post the background notice under the outcom…
HashEngineering Sep 25, 2026
6cd6c09
fix(sdk-bind): the queued foreground clear re-checks live visibility …
HashEngineering Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions DASHJ-KILL-LIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,125 @@ Most-used bitcoinj types repo-wide: `Coin` (105 files), `Transaction` (86), `Sha
with the engine); dashj's `BlockchainState` (sync %, replaying, impediments) must be fully
derived from `SpvSyncProgressData` (partially wired for the home header already).

#### 1a. BLOCKER on the balance replacement: partial rescan state is published as fact

`CutoverUiDataService` is the SDK-side replacement for the home-screen balance, so dashj cannot be
deleted until this is right — after step B there is no second engine to sanity-check it against.

Measured on a restored wallet, 2026-09-17 (see §26.4 of the upgrade memory and sync plan). Opening
the Contacts screen triggered a DashPay-provisioning rewind of ~214,000 blocks, and during the
re-scan the service published, in sequence:

| Published balance | In DASH |
|---:|---:|
| 28,214,058,609 | 282.1 |
| 24,233,701,645 | 242.3 |
| 21,351,026,841 | 213.5 |
| 16,902,773,043 | **169.0 — correct, matches pre-wipe exactly** |

For ~30 seconds the wallet displayed up to **113 DASH more than the user actually has**, stepping
down as the rescan re-derived the UTXO set. The end state is correct; the intermediate states are
not, and they are indistinguishable from real money on screen.

The signal to fix it with is already present: **every wrong value was published with
`l1Synced=false`.** Options are to suppress publication while that flag is false, hold the last
known-good figure, or mark the value as provisional in the UI. Today it emits partial rescan state
as though it were settled.

##### 1a.1 UPDATE 2026-09-19 — the source is the SDK's native ledger, and it does NOT self-correct

Reproduced on a Samsung SM-S901U on the job-flower wallet, and this time the end state was WRONG
too, which the 2026-09-17 reading was not.

```
18:47:57 107.08 DASH <- correct
18:52:54 107.08 <- app start
19:00:24 107.08 <- app start
19:11:02 107.08 <- app start
19:18:43 107.08 <- app start
19:29:34 110.07 <- replay to tip begins
19:29:56 1649.00
19:30:09 2942.25
19:31:07 3024.56 <- settles here, 28x too high, and STAYS past SYNCED
```

**The SDK's own database is correct throughout.** Pulled mid-defect and queried directly:

```
UNSPENT 811 txos 107.08173522 DASH <- the true balance, exactly
spent 9236 txos 193556.21892690 DASH
```

Both spend markers agree — `isSpent` and `spendingTxid` classify identically, zero disagreement.
So this is not missed spend detection, not double-inserted outputs, and not a persistence fault.

**It is not our code either.** `CutoverUiDataService.currentBalanceSplitDuffs` does no arithmetic:

```kotlin
// Native ledger read — the SAME accessor L1ShadowSyncService.sdkBalanceDuffs
// uses. A self-send debits this immediately; the Room `txos` table lags.
val balance = wallet.balance()
```

It publishes whatever `wallet.balance()` returns. The wrong number comes out of the SDK's
**in-memory native ledger**, which applies block events on top of an already-correct rehydrated
balance and therefore double-counts anything re-walked. This wallet re-walked 20,000 blocks after a
watchdog restart plus several full re-scans, which is why it reached 28x.

That also explains D-041's reported "only a force-stop fixes it": a restart rehydrates the ledger
from the `txos` table, which is why **every app start above reads exactly 107.08**.

**Consequences for this item.** The `l1Synced=false` suppression proposed above is necessary but NO
LONGER SUFFICIENT — the 2026-09-19 value was still wrong after SYNCED, so a wrong figure can now be
published with `l1Synced=true`. Two further mitigations are available on the app side while the
upstream fix is pending:

- reconcile against the `txos` table once `synced` is true and publish that instead; the table was
correct at every point measured, and 1a's own requirement is that this value be right before
dashj can be deleted;
- or treat a post-SYNCED disagreement between `wallet.balance()` and the table as a defect signal
and log it, so the field reports say which one was wrong.

**The real repair is upstream**, in `dash-spv` / `platform-wallet`'s `wallet.balance()`. Like the
filter-batch defect in §34 of the plan it sits below the FFI, so iOS is equally exposed. A cheap
reproduction for whoever takes it: force a re-walk (the filter-stall watchdog's restart does it),
watch the published balance climb, and confirm the `txos` table stays correct throughout — that
distinguishes it from a persistence bug in one pass.

Why this is a kill-list item and not just a bug: the rewind that exposes it is itself triggered by
ordinary DashPay provisioning (§17.6, §26.3), so it is not a rare path, and the pre-cutover
comparison against dashj's own balance (`L1Parity` probing) is the only thing that would currently
catch a regression here. That probe is suspended post-cutover and disappears entirely at step B.

#### 1b. DEAD PATH still shouting: the dashj broadcast handler warns that it did not send

`BlockchainServiceImpl.kt:2700`, inside the `ACTION_BROADCAST_TRANSACTION` handler:

```kotlin
if (peerGroup != null) {
peerGroup!!.broadcastTransaction(tx, minimum, true)
} else {
log.warn("peergroup not available, not broadcasting transaction {}", tx.txId)
}
```

Post-cutover `peerGroup` is always null, so **every send from a cut-over wallet logs that it is not
broadcasting** — while `SdkL1SendService` broadcasts it perfectly well through the SDK. The message
states the opposite of what happened.

The codebase already knows this: `SweepTxBroadcaster`'s KDoc says "Post-cutover that peergroup
broadcast no-ops ('peergroup not available, not broadcasting')" and routes around it. The warning
was simply never adjusted.

Measured 2026-09-17 (§27.5 of the upgrade memory and sync plan): a contact payment logged this
warning on the sender and was nonetheless confirmed on chain in block 1555561. It cost real time in
that test — the run was nearly written off as a failed send — and it will mislead anyone reading a
field log, which is exactly when this line gets read.

The whole handler is dead weight that dies with the engine at step B. Until then, either demote the
message and say the SDK owns broadcast now, or drop it when the cutover is committed. Cheap, and it
removes a false signal from every send log we collect between now and step B.

### 2. Key derivation / signing / seed handling
- **What it does**: `DeterministicSeed` (10 files), `KeyChainGroup`, `DeterministicKeyChain`,
`ECKey` (15), `KeyCrypterScrypt`/`KeyCrypterException` (17), `MnemonicCode`, `BIP38PrivateKey`,
Expand Down Expand Up @@ -110,6 +229,42 @@ Most-used bitcoinj types repo-wide: `Coin` (105 files), `Transaction` (86), `Sha
- **Blast radius**: `wallet/.../ui/dashpay/**`, `wallet/.../service/platform/**` — ~50 files,
plus the per-flavor `dash-sdk-{java,kotlin,android}` Gradle lines.

#### 5a. App-side DIP-15 friend-chain provisioning — now redundant (added 2026-09-16)

- **What it is**: `SdkWalletBinder.provisionContactAccountsIfEnabled` /
`provisionContactAccountsInBackground`, plus `DashPayBackfillGate`. It derives the
`m/9'/coin'/15'` per-contact keychains and registers them with the SDK so contact/username
payments land in the filter set.
- **Why it exists**: its own comment says it — "the app keeps DashPay contacts on dashj and
never drives the SDK's contact-sync path, so the bound SDK L1 wallet derives NONE of the
`m/9'/coin'/15'` friend chains and misses contact/username payments". It was the patch for
the SDK not being told the contacts existed. The exposure was real: 0.0836 DASH of
genuinely-ours coins missed at scan time on the topple wallet.
- **Why it is now redundant**: `1266edc1c` started calling the SDK's own ordered bring-up,
`PlatformWalletManager.startWalletSubsystems`, which does identity → contacts →
contact-account drain and reports `dashPaySyncRan` / `contactAccountsDrained` /
`contactAccountsPending`. Measured on emulator-5556, 2026-09-16: the bring-up reported
`status=READY dashPaySyncRan=true drained=14 pending=0`, and every app-side sweep afterwards
reported `pendingBuilds=0, drainScheduled=false`. The app sweep runs once a minute and finds
nothing. `DashPayBackfillGate` is already `ALWAYS_RUN`, i.e. a no-op shell: `evaluate` always
returns `shouldRun = true, reason = "backfill gate disabled"`, and every bookkeeping method
returns a constant.
- **The one job left**: `startWalletSubsystems` runs only at engine start
(`L1ShadowSyncService.startIfEnabled`, guarded by `if (!source.isSpvRunning())`), so a contact
established while the engine is already running is not picked up by it. `updateContactRequests`
calls the app sweep with `force = addedContact` for exactly that case. **Deleting the sweep
requires the SDK to re-run its contact sync on a contact change, or the app to restart the
engine on one.** Confirm with the SDK team before removing.
- **Kill**: delete `provisionContactAccountsIfEnabled`,
`provisionContactAccountsInBackground`, `DashPayBackfillGate` and its `ALWAYS_RUN` shell, the
`watchArmedBackfillRewind` poller, and the two call sites in `PlatformSyncService`
(`kickSdkEngines`, `updateContactRequests`). The rewind that catches payments missed before a
chain was derived is already the SDK's: it marks newly registered contacts at
`synced_height = 0` and its filter manager rescans via the account-generation guard
(`dash-spv/src/sync/filters/manager.rs`). That is also the single mechanism the whole
contact-payment recovery now depends on — see section 16 of
`docs/upgrade-memory-and-sync-plan.md`, which tests it.

### 6. Checkpoints / birth height / chain bootstrap
- **What it does**: `CheckpointManager` (3 files) + `wallet/assets/checkpoints{,-testnet}.txt`
fast-forward dashj's chain; `Constants.java` (11 bitcoinj imports) holds `NetworkParameters`.
Expand Down
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ buildscript {
kotlin_version = '2.1.0'
coroutinesVersion = '1.6.4'
ok_http_version = '4.12.0'
dashjVersion = '22.0.5-SNAPSHOT'
dashjVersion = '22.0.5'
// Dash Platform Kotlin SDK (org.dashj:dash-sdk-android). Single source of
// truth for both the dependency coordinate (wallet/build.gradle) and the
// DASH_SDK_VERSION BuildConfig field shown on the About screen once the
Expand All @@ -14,13 +14,13 @@ buildscript {
// #4439 (TXO reconcile) + the restore branch (#4474/#4475) + the ordered
// bring-up binding (#4658) + #4643 keystore fixes, on the fork engine pin
// af88edf6 (v4.2-dev pin 93260bf + rust-dashcore #979/#1001/#989).
dashSdkVersion = '0.1.0-v42int19-SNAPSHOT'
dashSdkVersion = '0.1.0-v42int21-SNAPSHOT'
// 4.0.1 fixes the CBOR crash that aborted every identity read:
// IdentityPublicKey.toObject() put the raw ContractBounds object
// (SingleContractDocumentType) into the map handed to Cbor, which has
// no converter for it -> IllegalArgumentException("No converter for ...").
// 4.0.1 calls this.toObject() first. Cbor.kt itself is unchanged.
dppVersion = "4.0.1-SNAPSHOT"
dppVersion = "4.0.1"
hiltVersion = '2.53'
hiltCompilerVersion = '1.2.0'
hiltWorkVersion = '1.2.0'
Expand Down
12 changes: 12 additions & 0 deletions common/src/main/java/org/dash/wallet/common/data/BaseConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ abstract class BaseConfig(
}
}

/**
* Apply several key writes in ONE DataStore transaction.
*
* [set] is a transaction PER KEY, so a group of keys that is only
* meaningful together can be torn apart by a process death between two
* consecutive calls, leaving a half-written record that still reads as
* valid. Write such a group through this instead.
*/
open suspend fun editPreferences(transform: (MutablePreferences) -> Unit) {
context.dataStore.edit { preferences -> transform(preferences) }
}

/** Remove [key] entirely, so a later [get] returns null (not a stale value). */
open suspend fun <T> remove(key: Preferences.Key<T>) {
context.dataStore.edit { preferences ->
Expand Down
73 changes: 73 additions & 0 deletions docs/kotlin-sdk-issues-to-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,76 @@ balance so integrators can validate the amount and show the correct "available"
balance overstates what's shieldable. Also: the error is a WalletOperation with the reason in the
message string; a typed InsufficientFunds error (with available/required) would let integrators
classify + display it without message-matching (relates to #14).

## 16. DIP-15 contact address pools never grow — the 21st payment from any contact is invisible

> **FILED** as dashpay/rust-dashcore#1032 (2026-09-16). The defect is in the `key-wallet` crate
> in rust-dashcore, not in platform, so it went there rather than on dashpay/platform. Note the
> root cause is narrower than first written: `AccountTypeToCheck` is a fieldless enum, so
> `extended_public_key_for_account_type` structurally cannot identify which contact account is
> meant. The fix belongs in `wallet_checker.rs`, which already holds the fully-keyed
> `AccountType`.

Live (testnet, two emulators, `rust-dashcore` rev `af88edf`): a DashPay contact chain watches
exactly 20 addresses, indices 0-19, and **never extends**. The 21st payment a contact sends
lands on index 20, which is never derived, so it is never matched by the filter scan and never
enters the balance. It is unrecoverable by rescanning, because the address does not exist
client-side. Money-visibility bug, not latency.

Three things compose:

- `key-wallet/src/wallet/helper.rs:612` — `extended_public_key_for_account_type` returns `None`
for `DashpayReceivingFunds` and `DashpayExternalAccount`, commented "Currently not retrieved
via this helper". So `key_source_for_account_type` yields `KeySource::NoKeySource`.
- `key-wallet/src/transaction_checking/wallet_checker.rs:357` — `if matches!(key_source,
KeySource::NoKeySource) { continue; }` bails **before** `maintain_gap_limit`. Note
`mark_address_used` runs *above* the guard, so used flags advance normally while the window
does not. That divergence is what makes the bug hard to see.
- `key-wallet/src/managed_account/managed_account_type.rs:726` and `:745` — both DashPay pools
are built via `single_pool(..., 20, ...)` with a **hardcoded literal**, not a named constant.
`maintain_gap_limit` with `highest_used == None` targets `gap_limit - 1` = 19, which is
exactly the observed pool.

Measured on a wallet with 14 contact accounts: every one sits at top index 19 regardless of
use. Driving six payments onto one chain advanced `isUsed` to index 5 while the top index
stayed 19. A sliding pool (`highest_used + gap_limit`, as `maintain_gap_limit` documents)
would have reached 0-25.

Requests:

- Make the checker reach `maintain_gap_limit` for the two DashPay pools. The material is
already persisted — every contact account row carries `accountExtendedPubKeyBytes`
(104 bytes) — and friend-chain receiving addresses derive from that xpub alone, so no seed
and no device unlock is needed. This is unfinished wiring, not a cryptographic limit.

Resolve it in `wallet_checker.rs`, **not** by teaching
`extended_public_key_for_account_type` to return the contact xpub. That helper takes
`AccountTypeToCheck`, which is a FIELDLESS enum: given only `DashpayReceivingFunds` it
cannot say *which* contact account is being asked about, so there is no correct value for it
to return. `wallet_checker.rs` already holds the fully-keyed `AccountType`, so either it
does the lookup itself, or it passes that keyed value down — but the fieldless helper cannot
be the place the decision is made.

(An earlier revision of this section asked for exactly that helper change. The note at the
top of this issue corrected the root cause; this bullet had not been brought in line, so the
issue as filed still points implementers at an API change that cannot fix the defect.
rust-dashcore#1032 needs a comment saying so.)
- Replace the hardcoded `20` with a named constant. Note `DEFAULT_CONTACT_GAP_LIMIT = 10`
exists in `rs-platform-wallet/src/wallet/identity/crypto/dip14.rs:254` (quoting DIP-15's
recommendation) but has **no consumer** — neither it nor `derive_contact_payment_address`
is called outside its own module. Decide which value is intended and wire one of them.
- Consider whether `mark_address_used` should stay above the `NoKeySource` guard. Advancing
used-state for a pool that can never be maintained produces a database that looks healthy
while the watch set is stale.

Also affects `dashpayExternalAccount`, so a wallet stops tracking its own outgoing contact
payments past index 19.

**Reproduced end to end (testnet, 2026-09-16).** Twenty sequential contact payments filled
indices 0-19 and were all received. The 21st, `b837bcb2f4f50c29502cd32549b3c369f28157b4ead872b7f41351cebb121ab0`
(29,000 duffs to `yR3kpYnbcPipq4982WcEK2mCBrQXh7P39K`, block 1555216, 2 confirmations), was
never seen. With the receiving wallet's service running and synced through block 1555217,
its database holds zero `transactions` rows, zero `txos` rows, and — decisively — zero
`core_addresses` rows for the destination address. The address was never derived, so the
BIP158 scan could not match it. Coins are on-chain and spendable by the seed, but invisible
to the wallet permanently and unrecoverable by rescan.
Loading
Loading