Repository navigation
Conversation
j0ntz
marked this pull request as ready for review
September 8, 2026 18:57
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
3 times, most recently
from
September 10, 2026 02:45
05c0638 to
b4eaaad
Compare
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
2 times, most recently
from
September 10, 2026 06:04
c8b71e8 to
07ffdd4
Compare
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
3 times, most recently
from
October 7, 2026 07:48
419dd62 to
32b05ae
Compare
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
3 times, most recently
from
October 28, 2026 23:20
8ccc97c to
2f1f5f6
Compare
The @typescript-eslint plugin/parser/eslint trio was inconsistent: plugin 2.34.0 wants parser ^2 and eslint ^5||^6, while the manifest pinned parser ^3.0.1 on eslint ^7. Both install paths failed with ERESOLVE, so the documented build-and-smoke flow could not run at all. Align the set to the one react-native-zano already uses (typescript-eslint 5.x on eslint 8, standard-kit 0.15.1, TypeScript 5.0.4), rename the renamed simple-import-sort rule, add a lint-only tsconfig so the scripts directory parses, and clear the findings the newer rules surface.
The Halo 2 proving key build is the heaviest operation in the module and the one a spend cannot skip, so the smoke test now warms it and asserts the readiness flag flips, reporting the elapsed time. Also record the protoc build prerequisite, which the cargo step fails without.
The pool could only ever be empty: create_transfer spends notes, and nothing in the module put notes there. Value reaches Orchard through Dash L1 only, so this adds the three pieces that path needs. coreReceiveAddress exposes the wallet's transparent BIP-44 address, which a faucet or any sender can pay. startCoreSync runs the Core SPV client the manager already holds but never started, since the Platform SDK connection cannot see L1 and the transparent balance stays at zero without it; a start height keeps a fresh wallet from scanning filters it can never match. shieldFromAssetLock then builds and broadcasts the L1 lock, waits for its InstantSend or ChainLock proof, and proves a Type 18 transition paying the wallet's own Orchard address. Signing the lock needs private keys the PlatformWallet deliberately does not hold, so a seed-backed signer re-derives them for the call, matching what create_transfer already does for the Orchard spend authority. scripts/fund-testnet.ts drives the whole path so the shielded send has something to spend.
Quorum-discovered nodes were dialled at https://<ip>:443, but testnet Platform gRPC listens on 1443. Measured against the live quorum list: 0 of the 30 ENABLED, version-checked masternodes accept a connection on 443 and all 30 accept on 1443, so every DAPI call on testnet was aimed at a dead port. It surfaced as a tonic tcp connect timeout out of shieldFromAssetLock, after the L1 asset lock had already broadcast. The port now comes from the network, with a caller-supplied port still winning for devnets, and the scheme follows the port rather than assuming plain HTTP for anything that is not 443. Stopping a wallet also dropped the manager while the shielded-sync task still held a tokio timer, panicking that task with 'A Tokio 1.x context was found, but it is being shutdown'. stop now signals the loop and awaits quiesce first.
shield_from_asset_lock broadcasts the L1 lock first and proves the Type 18 transition second, so a failure between the two strands the locked value: the duffs are spent, nothing credits the pool, and a fresh lock cannot recover them. resume_shield_from_asset_lock finishes that stranded outpoint. tracked_asset_locks lists what there is to resume, without which the resume call is unusable: the failing shield returns an error, not the lock it had just broadcast. unshield and shielded_withdraw are the exits, to a Platform address and to an L1 address respectively. Resume reaches only locks this process built. Opening a wallet now loads from the persister first, but that restores nothing yet for an upstream reason: SqlitePersister::load declares LOAD_UNIMPLEMENTED = ["ClientStartState::wallets"] and rebuilds only platform_addresses, so the asset_locks rows it writes are never read back. Calling load first costs one query and is what makes resume survive a restart once upstream lands rehydration.
The UniFFI side had drifted behind the N-API side: the Core sync, funding, and pool-exit entry points existed only for Node. Declaring them in the UDL and wrapping them in uniffi_api regenerates dash.swift and dash.kt, and the iOS and Android bridge modules pass them through, so React Native reaches the same seven calls Node does.
Both hosts get the same seven methods, so a caller written against the Node Synchronizer moves to React Native unchanged.
A copied dylib keeps its original linker-signed signature, which macOS rejects: require() of the prebuild died with SIGKILL and no output at all. Re-signing the destination ad hoc makes it loadable.
edge-react-gui ships armeabi-v7a alongside arm64-v8a (its abiFilters and reactNativeArchitectures both name the pair), so an arm64-only library left every 32-bit device without one. The Orchard and Halo 2 stack compiles for 32-bit ARM, so the slice does not have to be dropped.
start_sync started the manager's sync loop, whose first act is a pass, and then ran a second pass straight on the coordinator. Only ShieldedSyncManager::sync_now holds the in-flight slot, so the two passes overlapped. Upstream reads the tree size before it takes the store's write lock, so both passes saw the old size and each appended every new commitment. On a new store that left 9,506 local leaves against Platform's 4,753. The first spend still landed through the depth-1 checkpoint. After it, no new commitment was appended, the change note's witness never matched the tree root, and every later spend failed with 'no recorded anchor covers the selected notes'. Run the immediate pass through sync_now(true), waiting out a pass already in flight. Verified on testnet: the local tree size and depth-0 root equal Platform's notes count and most recent anchor before each of five consecutive spends.
At v4.1.1 a new process restored nothing from the wallet store, so an asset lock broadcast by an earlier process was not tracked and could not be resumed. The 4.2 line restores the wallet, its address pool and its tracked locks on open. Resuming the stranded testnet lock c12094b3 from a new process succeeded on v4.2.0-beta.7 and returned "is not tracked by this wallet" on v4.1.1 against the same store history. A lock rebuilt that way carries upstream's RecoveredFromChain status, which the listing now names recovered_from_chain. v5.0.1 is the first release with dashpay/platform#5352, which lets the shielded store open under Android's app data directories. It needs rust-dashcore d631aaca and Rust 1.98.1. ShieldedSyncManager::quiesce now returns false when the in-flight pass did not drain within its budget, and PlatformWalletManager::shutdown returns a report of any worker still alive. stop now returns an error when either says the close was not clean, instead of STOPPED.
The manager's wallet map and the wallets in it hold each other, so dropping the manager after shutdown left the SqlitePersister alive. v5's per-process open registry then refused every reopen of that alias with "a SqlitePersister is already open". stop() now removes each wallet from the manager before dropping it and fails if the store is still referenced afterwards. The Node smoke test reopens the wallet after stopping it.
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
from
October 28, 2026 23:43
2f1f5f6 to
5929e66
Compare
Author
|
bugbot run |
|
Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Technical Design Document
dash-shielded-wallet.md
Description
Asana task
The base is
paul/dashShielded, which carries the module. This branch makes its dependencies install, then adds the calls a wallet needs to fund a shielded balance from L1, recover a stranded lock and exit the pool, on the Node addon and on the React Native bindings. It also moves thedashpay/platformpin fromv4.1.1tov5.0.1, the first release with dashpay/platform#5352, and makesstopclose the wallet store.standard-kit0.15.1 and TypeScript 5.0.4. The smoke test now warms the prover and asserts readiness.coreReceiveAddress,startCoreSync,coreBalanceandshieldFromAssetLockbuild an asset lock from the wallet's own L1 balance and shield it.scripts/fund-testnet.tsdrives the flow.trackedAssetLocksandresumeShieldFromAssetLockfinish a shield whose lock was already broadcast.unshield,platformReceiveAddressandshieldedWithdraware the exit calls.mnemonicfield is gone.startSyncran its immediate pass straight on the coordinator while the sync manager's loop ran its own first pass. Upstream reads the tree size before it takes the write lock. The overlapping passes each appended every commitment, and the wallet tree held twice Platform's count (9506 against 4753). No anchor matched, and later spends failed withno recorded anchor covers the selected notes. The immediate pass now goes throughShieldedSyncManager::sync_now, which refuses to overlap a pass in flight.rs-sdk-trusted-context-provideris built with itswithdrawals-contractfeature. Without it a withdrawal landed on chain and the call still returned an error, because the result could not be verified.v5.0.1needs Rust 1.98.1 and rust-dashcored631aaca. From the 4.2 line on, a new process restores the wallet's Core state and its asset locks from the store, whichv4.1.1did not do. The 4.2 line also added a directory check that refused the store on Android;v5.0.1trusts Android's system-owned/dataand the app's own group, and the store opens in the app'sfilesdirectory.v5.0.1the wallet manager and each wallet hold references to each other, and the store registers its path once per process. Dropping the manager left the store open, and opening the same wallet again in that process was refused witha SqlitePersister is already open.stopnow shuts the manager down, removes each wallet, drops the manager and checks that nothing still holds the store. It returns an error when a sync worker did not shut down cleanly.src/react-native.tscover the same surface as Node. The copied macOS addon is ad hoc signed, and Android builds and packagesarmeabi-v7aas well asarm64-v8a.Physical Android phones at
v5.0.1edge-react-gui(developat982bc31ee) was built with this branch linked as a local dependency and a local test script that runs at app start. None of that is committed. The releasearm64-v8aAPK ran on a Google Pixel 10 and a Samsung Galaxy S26, both Android 16, rented from a device cloud. The script uses the public BIP-39 test vector on testnet, which holds nothing, because the APK went to a third party's phones. It warms the prover, opens the store, syncs, stops, opens the same wallet again and stops a second time.stopstopSYNCEDat 651816SYNCEDat 651816The store opened under
/data/user/0/co.edgesecure.app/fileswith no change to any directory. Before thestopchange the same phones reportedSTOPPEDand then refused the reopen (139 ms on the Pixel, 38 ms on the S26).A spend run followed at the same code. The Node host sent 2,000,000,000 credits from the funded testnet wallet to the test vector's shielded address (
37c9c516dfc3c2a621634f84e78603bd4a3167a55d8e427c41367ae45260adc5). A new APK, still carrying only the test vector, then ran on the Pixel 10:SYNCED, 2,000,000,000 credits at 651835c2b96646c78917716220e027e315d573689252f7d0bce7506018d130a9ebe88bunshieldof 300,000,000 credits to its own Platform address, once the change note was minedstop, reopen, secondstopSTOPPED, same address,STOPPEDunshieldreturns the destination address rather than a transaction id. A host sync afterwards read 300,000,000 more credits on the funded wallet and 1,165,637,200 on the test vector: 1,585,860,000 change, less the 300,000,000 unshielded and a 120,222,800 fee.A funding run followed. The Node host withdrew 4,000,000,000 credits from the funded wallet to the test vector's first Core address, and L1 transaction
1123be375c47b883d88c074bca37477e985c191a32056fd027a6f27d50aba04cpaid it 3,999,810 duffs. Two more test-vector APKs then ran on the Pixel 10, each on a new store:SYNCEDin 22706 ms at 651850SYNCEDin 27015 ms at 651854shieldedWithdrawof 500,000,000 credits to its own Core addressshieldFromAssetLocktrackedAssetLocksconsumedconsumed, and the first run's lock asrecovered_from_chainresumeShieldFromAssetLockon its own lockcannot be reused; Platform completion is unconfirmedstopSTOPPEDin 96 msThe refusal is upstream's
AssetLockAlreadyConsumed, whichv4.1.1wordedhas already been consumed. The first script matched only the old wording and stopped at the resume; the second matched both. The first run's withdrawal reached L1 as asset-unlock transaction21b1fc6965a236cc3ee8574133f02b7951eb26a56f7fb9ac3d580bf4613da52a, paying 499,810 duffs.Release split APKs at
v5.0.1, against Google Play's 100 MB limit per APK:app-arm64-v8a-release.apkapp-armeabi-v7a-release.apklibdashshielded.socompresses to 8.8 MB in thearm64-v8asplit and 7.9 MB in thearmeabi-v7aone.Earlier pins
On testnet from the Node addon at
v4.2.0-beta.7, on a new store, each of these was accepted by Platform:shieldedWithdrawof 1,000,000,000 credits to L19a862caf4a7f0d008946b5f003e55eeda0cf628d5cb9a5514706f89adb522dadunshieldof 500,000,000 credits4cc80a45c8e2945af5b5e8c6a7dd5b77efc9c485e66bfaf780da27930da094a3b440f3ded5238f38f08765ae916771aee9478f914e9ba0126f4e586ff88bcc48With the sync fix alone on
v4.1.1, four spends in a row were accepted, and the wallet tree size equaled Platform's notes count before each one. In a new process onv4.2.0-beta.7, the stranded lockc12094b3...52f8:0was listed and resumed in 4.4 seconds. The same step onv4.1.1listed no locks.At
v4.2.0-beta.7the Edge app on an iOS 18.6 simulator and an Android 16 emulator sent a shielded transfer each (3514 ms and 4241 ms), after the directory check was satisfied by hand.Not yet proven:
v5.0.1, resuming a lock that was truly stranded, and resuming one rebuilt asrecovered_from_chainafter Platform consumed it.armeabi-v7aandios-arm64libraries. Both are packaged, and nothing has loaded them.recovered_from_chainwhether or not Platform has consumed it. Resuming a spent one returns an error and moves no value.proposeTransferquoted a fee of 114,140,000 credits, and each mined transfer cost 162,851,200.Test evidence
5929e66Release the wallet store when a wallet stops
2026-10-28
1. pixel10 probe pass
2. s26 probe pass
4. pixel10 spend pass
5. pixel10 fund pass