Conversation
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.
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
from
September 8, 2026 18:57
7197fcf to
6e2d986
Compare
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.
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
2 times, most recently
from
September 10, 2026 02:45
05c0638 to
b4eaaad
Compare
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.
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
from
September 10, 2026 03:09
b4eaaad to
c8b71e8
Compare
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.
j0ntz
force-pushed
the
jon/dash-shielded-install-and-smoke
branch
from
September 10, 2026 06:04
c8b71e8 to
07ffdd4
Compare
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.
Description
Eight commits, in the order the module needed them.
Dependency install. The documented host flow in the README could not run:
both dependency-install paths failed with
ERESOLVE.@typescript-eslint/eslint-plugin@2.34.0requires@typescript-eslint/parser@^2oneslint@^5 || ^6, while the manifest pinnedparser
^3.0.1oneslint@^7. No combination of the three resolves, so nothingdownstream of install had ever been exercised. Moving the parser alone fixes it
in neither direction. The lint toolchain now matches the set
react-native-zanoalready ships: typescript-eslint 5 on eslint 8,standard-kit0.15.1, TypeScript 5.0.4. That brings a renamed rule(
simple-import-sort/sorttoimports) and a lint-only tsconfig, since thebuild tsconfig excludes the
scriptsdirectory.Prover coverage. The smoke test warms the Halo 2 proving key and asserts the
readiness flag flips. That is the heaviest operation in the module and the one a
spend cannot skip. It takes about 1.4 seconds on an Apple Silicon host.
Funding. The pool could only ever be empty.
create_transferspends notes,and nothing in the module put notes there. Value reaches Orchard through Dash L1
only, which needs three pieces:
coreReceiveAddressexposes the wallet's transparent BIP-44 address, so afaucet or any sender has something to pay.
startCoreSyncruns the Core SPV client the manager already holds but neverstarted. 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.
shieldFromAssetLockbuilds and broadcasts the L1 asset lock, waits for itsInstantSend or ChainLock proof, then proves a Type 18
ShieldFromAssetLocktransition paying the wallet's own Orchard address.
Signing the lock needs private keys the
PlatformWalletdeliberately does nothold, since it is created external-signable. A seed-backed signer re-derives
them for the duration of one call, the same posture
create_transferalreadyuses for the Orchard spend authority.
scripts/fund-testnet.tsdrives the wholepath: it prints the L1 address, starts the Core sync, waits for the payment,
shields, and reports the resulting shielded balance.
DAPI port. Quorum-discovered nodes were dialled at
https://<ip>:443, buttestnet 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, afterthe 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.
stopalso dropped the manager while the shielded-sync task stillheld a tokio timer, panicking that task; it now signals the loop and awaits
quiesce first.
protocis an undocumented build prerequisite.dash-sdkpullsdapi-grpc,which compiles protobuf definitions during the cargo build, and the README now
says so.
Pool exits and resume. Value could go into Orchard and move within it, but
never leave, and a shield that failed halfway could not be retried.
shieldFromAssetLockbroadcasts the L1 lock first and proves the Type 18transition second, so a failure between the two strands the locked value: the
duffs are spent into the lock, nothing credits the pool, and building a fresh
lock cannot recover them. Three entry points close that:
resumeShieldFromAssetLockfinishes a shield against an outpoint whose lockis already on chain. This is what makes a retrying auto-shield host safe to
write, since a crash mid-shield otherwise loses the funds.
trackedAssetLockslists what there is to resume. Without it the resume callis unusable: the failing shield returns an error, not the lock it had just
broadcast, so a host has no way to learn the outpoint.
unshieldspends notes to a transparent Platform address, andplatformReceiveAddressproduces one to send to. It derives under DIP-17 frompublic key material, so no seed is touched.
shieldedWithdrawspends notes out to a Core L1 address, priced by aduffs-per-byte fee rate for the L1 transaction the network builds on the far
side.
All four take the mnemonic as a call parameter rather than reusing the one
initializewas given.ClientSlotcarried amnemonicfield that nothing onthe base branch ever read: its only spend path,
create_transfer, already takesthe seed per call. Reading that field here would have made an alias string
sufficient to move funds, and an alias is not a secret, so the field is gone and
the four exits follow
create_transfer.SeedSignermatters most: it signs anypath the asset-lock orchestrator asks for, so it reaches transparent balance
too.
For the same reason, quorum-discovered masternodes are pinned to
httpswhatever port they answer on. Deriving the scheme from the caller's port, as an
earlier commit on this branch did, would silently put real mainnet or testnet
gRPC in cleartext on a non-standard port, and a shielded wallet's request stream
is the metadata the pool exists to hide. The port-implies-scheme heuristic now
covers only the caller's own fallback host, as it did before.
Opening a wallet now loads from the persister before falling back to seed
creation. That restores nothing yet, and the reason is upstream:
SqlitePersister::loaddeclaresLOAD_UNIMPLEMENTED = ["ClientStartState::wallets"], rebuilds onlyplatform_addresses, and logswallets_rehydrated = 0. It writes durableasset_locksrows and never readsthem back, so tracked locks cannot survive a restart and a resume reaches only
locks its own process built. Calling load first costs one query and is what
makes resume survive a restart the moment upstream lands rehydration.
React Native parity. The UniFFI side had drifted behind the N-API side: the
Core sync, funding, and pool-exit calls existed only for Node. Declaring them in
rust/src/dash.udlregeneratesios/dash.swiftand the Kotlin bindings, andthe iOS and Android bridge modules pass them through, so React Native reaches
the same calls Node does.
ios/dash.swiftwas stale on the branch and isregenerated here. The module generates its own Swift and Kotlin from the UDL,
so it never depended on Dash shipping either SDK.
Both native artifacts build for the first time in this branch.
ios/libdashshielded.xcframeworkcarriesios-arm64andios-arm64-simulatorslices at 32 MB each. Android now builds both ABIs
edge-react-guiactuallyships, since its
abiFiltersandreactNativeArchitecturesboth namearmeabi-v7aalongsidearm64-v8aand an arm64-only library left every 32-bitdevice without one: 13 MB aarch64 and 11 MB ARM EABI5, both stripped. The
Orchard and Halo 2 stack compiles for 32-bit ARM, so that slice does not have to
be dropped. Every rust target has to be installed on the 1.97.1 toolchain
rust/rust-toolchain.tomlpins rather than the default stable, or both buildsfail with
E0463: can't find crate for std.macOS prebuild signing.
require()of the built addon died withSIGKILL,no output and no error. A copied dylib keeps its original linker-signed
signature, which macOS rejects on load. The build script re-signs the
destination ad hoc after the copy.
Asana: https://app.asana.com/0/1215088146871429/1216611553286539
Test plan
Static, from a clean checkout on macOS arm64:
socket npm installresolves 638 packages where it previously exitedERESOLVEsocket npm run lintreports nothingsocket npm run prepare(rollup + tsc) passessocket npm run build-native-hostproduceslibdashshielded.dylibsocket npm run smoke-nodeprintssmoke-node okwith a derived testnetaddress, the viewing key, and
proverMsaround 1430~/.cursor/skills/verify-repo.sh . --base origin/paul/dashShieldedpassesEnd to end on live Dash testnet, with the wallet funded by 1 tDASH:
The Core SPV client reaches testnet tip and sees the transparent balance, the
asset lock broadcasts and resolves its proof, the Type 18 transition proves and
is accepted, and the resulting note decrypts under the wallet's own viewing key.
A shielded transfer then spends that note:
Both proofs run in roughly 4 seconds, warm prover excluded.
The per-call seed is what authorizes a spend now, not the alias. Calling
unshieldon an open wallet with a wrong phrase is refused atinvalid mnemonic: BIP-0039 mnemonic only supports 12/15/18/21/24 wordsbefore any noteis touched.
build-native-ios.tspinsIPHONEOS_DEPLOYMENT_TARGETto 15.6 and both slicescarry
minos 15.6, matching what Edge targets. Dash's ownpackages/swift-sdkdefaults to 17.0 and declares iOS 18, but this module never uses that wrapper:
it generates its own Swift from the UDL, so that floor does not apply here.
No native artifact can run on hardware here, so their FFI surfaces are checked
statically instead: every symbol the generated bindings load is diffed against
what the library exports. The Kotlin bindings reference 103 symbols and both
Android ABIs export exactly those 103, zero missing. The Swift bindings
reference 49 and both iOS slices export 103, zero missing. That is what catches
a
libraryOverridename mismatch or an ABI-specific codegen gap, which wouldotherwise surface only as an
UnsatisfiedLinkErroron a device.The macOS addon signing is verified by deleting
prebuilds/darwin-arm64/,rebuilding, and running the smoke test:
codesign -dvreports anadhocflagand
smoke-nodeexits 0 withproverMs1464, where the unsigned copy waskilled at
require().Resume and the lock listing, driven in one process on testnet:
Two things are proven there.
LOCKS_BEFOREis empty on a store whoseasset_lockstable holds a row, which is the upstream rehydration gap seen fromthis side. And the resume of a lock this process built reports
AssetLockAlreadyConsumed, notAssetLockNotTracked, so the lookup finds thelock and the resume path is correct. Every earlier attempt reported
AssetLockNotTrackedonly because it ran after a restart.Not yet proven
shieldedWithdrawreturnsShielded spend cannot use a Platform-recorded anchor: no recorded anchor covers the selected notes. Four drives bound it.A transfer and a withdraw in one process on one synced store: the transfer
landed txid
4f7f10de7da3e818d4507e1ee63d5f84749803856e45997fa5dd17e39ddd329bin 4132 ms and the withdraw after it failed, which reads as withdraw-specific
until the same pair with the order flipped fails on both. They share
extract_spends_and_anchor, so it is neither operation. Amounts of 1000000,100000000, 1000000000 and 5000000000 credits fail identically, so it is not
the size of the note selection. Five attempts over fifteen minutes at an
unchanged balance all fail, so it is not the transient the message describes.
The condition is that no checkpoint depth in this store has a root Platform
recorded, and the probe bound
MAX_ANCHOR_PROBE_DEPTH = 100matches theretention
configure_shieldedopens the tree with, so the module is notmisconfiguring it.
unshieldreaches the same anchor wall. Driven against a real Platformaddress from
platformReceiveAddress(
tdash1krxraw6u2wczgp956t7mpn2pg05l2uzxk57hdvlx), it builds the spend andfails where the other two do, so all three shielded spends share one upstream
condition.
ClientStartState::wallets. The call is correct; the state it needs is notrestored.
The base is
paul/dashShieldedrather thanmaster, since that branch carriesthe module.