Conversation
- ZSA asset tracking in coin selection (asset_index on notes/outputs) - Per-asset balance and change computation - ZSA memo decryption (612-byte ciphertexts via OrchardZSADomain) - Wire format: read/write full ZSA enc_ciphertexts with asset field - V2/V3 bundle signing: add_orchard_change_output for Ironwood/V3, add_orchard_output for V2 (IO Finalizer handles dummies) - Sync refactor: CompactTx instead of SyncTx, drop issuance synthesis - Schema: UNIQUE(asset_base), idx_assets_asset_base, asset_name column - Cargo.toml: switch path overrides to git revs (orchard bd4be3b, lrz f53afe2a)
…uance; add zsa-circuit feature; add ZSA issuance test
… CLI - All Nu7 orchard transactions use ORCHARD_ZSA_PK (ProvingKey::build_zsa()) - Add is_zsa() helper to detect Nu7 consensus branch - Add pczt_replay CLI binary for reproducible PCZT proving - Add test_dump_instances test and PCZT save-to-file in tests
…T domain support - TransactionData.orchard_bundle is now Option<OrchardBundle<A::OrchardAuth>> - OrchardBundle enum with OrchardVanilla/OrchardZSA variants - All callers updated to match on variant - ZSA commitment/digest support with 612-byte ciphertext indices - PCZT Output::parse now generic over Domain for ZSA ciphertext parsing - write_v6_bundle_zsa accepts raw ZSA enc_ciphertexts
…domain support, test: block-mining wait loop, per-account coins
- orchard: zcash-shielded-assets/orchard @ 7ba8e8a - lrz crates: zcash-shielded-assets/librustzcash @ f90335ab
…it loops in tests
- orchard: zcash-shielded-assets/orchard @ 8c1c9bee - lrz: zcash-shielded-assets/librustzcash @ b2d59ba8
- migrate: use mounted check instead of context.mounted for error display - frost: use orchard_pk from PCZT consensus branch instead of network check - plan: replace unwrap() with map_err, add OrchardProvingKeyKind, PCZT-DUMP diagnostics
Drop the fee-vs-privacy coin-selection toggle and collapse the solver to a single strategy: minimize cross-pool turnstile value, then minimize the ZIP-317 fee among equally private solutions. Solver (rust/src/pay/solve.rs): - evaluate_privacy now breaks change-pool ties on fee instead of keeping whichever pool it reached first. - Branch-and-bound compares (turnstile, fee) lexicographically and keeps expanding nodes whose bound equals the incumbent, since an equal-privacy subtree can still yield a cheaper fee. - The reported fee is the fee of the selected change pool. It used to be recomputed as the globally cheapest pool, which could pair the privacy-optimal change pool with a fee computed for a different one. - Remove Mode, evaluate_fee, lower_bound_fee, compute_min_fee, the fee branch of local_score, and the unused Selection::change_amount. - Add tests for the fee tie-break and the equal-bound acceptance rule. Plumbing: drop the mode parameter from plan_transaction, PaymentOptions and the GraphQL Payment input, remove getCoinSelectionMode/setCoinSelectionMode and the coin_selection_mode DB property, and delete the "Privacy Preservation" switch from the send page. Callers that previously requested Mode::Fee — asset issuance, note migration, prepare_migration — now go through the privacy-first solver.
* chore: update build number / release * chore(main): release zkool 6.25.0
Add a "nym" cargo feature (on by default) covering the mixnet transport and nym:// nym-rpc endpoints, making the nym crates optional dependencies. CI graphql builds now pass --no-default-features so the server no longer compiles the Nym stack or the Flutter bridge.
The transparent sweep stores every used external address but left accounts.dindex untouched, so addresses found above the current index were missing from the 0..=dindex address list and next-address generation could land on an already-used index. After the scan, bump accounts.dindex to the greatest stored external (scope 0) index, never backwards.
The gap counter was cumulative: used addresses found between unused ones did not reset it, so a sparse used/unused pattern ended the scan early and never discovered later used addresses. Reset the gap on each used address.
Unshield All used sourcePools 6 (Sapling | Orchard) and silently left any Ironwood shielded balance untouched. Use 14 (Sapling | Orchard | Ironwood) so the whole shielded balance is swept to the transparent address.
autoSync() read the interval from appSettingsProvider, which only updates after the settings page is saved, so changing the interval from 0 to a positive value cancelled the subscription instead of starting it. Pass the new interval explicitly to autoSync().
After restoring an account from a seed phrase, the 'synchronize now' prompt did nothing: syncIfNeeded read a stale getAccountsProvider cache that lacked the new account. Refresh and await the account list before switching the selection, and set selectedAccountIdProvider so the restored account is the selected one (also fixing resume-after-restart and the null currentAccount crash when the offstage AccountViewPage rebuilt mid-restore).
(cherry picked from commit 36e72b5)
sign_transaction fuses signing, proving and spend finalization, so a PCZT signed off-device (Keystone, Cupcake) had no way to be completed: proving is hot-side work but was only reachable through the code path that needs spending keys. prove_and_finalize takes an already-signed PCZT and runs just the Prover (sapling/orchard/ironwood) and SpendFinalizer, so the result can go to extract_transaction and broadcast. It touches no spending keys, making it usable on watch-only accounts, and is exposed through api::pay for FRB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 2a52d2e)
The PCZT's transparent bip32_derivation entries carried a zeroed seed fingerprint and a two-element path. Nothing checked either while signing happened in-process, so both were effectively placeholders. A hardware wallet does check: it matches the fingerprint against its own seed and parses the path as BIP 44, and rejects anything that fails as belonging to a different wallet. Write the account's stored fingerprint and the full m/44'/coin'/account'/scope/index path instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 972c702)
Orchard and Ironwood spends were built with no ZIP-32 derivation, because the Orchard updater was never run at all — only the transparent and sapling ones were. A shielded spend therefore reached an external signer with nothing identifying the account that owns it, and was refused as belonging to a different wallet. zip32_derivation is the only ownership marker the Updater role can place on an Orchard spend, so set it on every real spend in both bundles. The path is m/32'/coin_type'/account', fully hardened and exactly three elements — the five-element BIP 44 path transparent inputs use is rejected here. Dummy spends are builder padding and stay unclaimed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 2fbdb98)
Cake pins the lrz fork of pczt 0.7; a Keystone running Cypherpunk firmware speaks 0.8.0-rc.1. Both call their encoding "v2", but six fields differ: anchor, cv_net, nullifier, rk and cmx each gained an Option, and enc_ciphertext became an enum. postcard is positional and not self-describing, so an Option's tag byte shifts everything after it. The result is a silent misread rather than an error. A Keystone parses a PCZT Cake built without complaint and finds zero actions in every pool, so it reports that none of the inputs belong to the wallet -- which surfaces as "Incongruent Transaction" with nothing to suggest an encoding problem. Translate at the QR boundary rather than moving either side's pinned version: Cake's ZSA features and the device's firmware both stay put. The ZSA issue bundle has no counterpart on the device and is dropped; it is empty for any transaction the device could sign anyway. Verified against the firmware's own parser, which reads two Ironwood actions and one signable spend from the transcoded fixture where Cake's dialect yields none. The reverse direction reproduces the original bytes exactly, so the returned signature carries back losslessly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit e5fa0a0)
A signer returns what it needed to produce, not the document it was given: the Keystone redacts prover-only fields, including the Orchard full viewing key. Adopting its reply wholesale therefore loses data proving requires, and the spend fails at the prover with MissingFullViewingKey after the user has already approved it on the device. Keep the PCZT this wallet built and merge in only the authorising signatures. A spend that already carries one keeps it, since the IO Finalizer signs dummy spends before the device ever sees them, and a reply whose shape does not match the transaction that was sent is rejected rather than partially applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 35b5361)
decrypt_memo computes each transaction's fee and writes it to transactions.fee, but fetch_txs never selected the column, so every consumer saw zero and had no way to recover the real value: outputs the wallet sent are stored separately from notes it received, making the fee derivable only by combining three lists. Select the column and carry it on Tx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 5afa396)
An external signer displays each transparent output's recipient for review and refuses one it cannot name: keystone3-firmware rejects a PCZT whose transparent output has no user_address, whether or not the output belongs to the wallet. The address is recoverable from the script, but the signer will not derive it, so any transaction creating a transparent output — deshielding, or paying a t-address — fails on the device. State the address alongside the script. Outputs whose script is not a recognised transparent address are left alone rather than guessed at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 4b2a164)
Adds to_batch_request and apply_batch_sig_result in keystone_wire, exposed as
pczt_to_batch_request / pczt_apply_batch_signatures. The request carries the
device's v2 PCZT dialect headerless under one shared version ("PCZB" magic);
the reply is only Orchard/Ironwood spend-auth signatures ("PCZS"), applied to
the wallet-owned PCZT -- an order of magnitude fewer QR frames on the return
leg. Shielded spends only; a transparent-input shield stays on zcash-pczt
(firmware rejects transparent inputs in a batch). Byte-exactness of the batch
inner PCZT is asserted against the device-verified golden fixture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 880ff9cefb9e5a670b85330e40073e60428bb0f8)
The device rejects a batch request carrying any Orchard/Ironwood spend-auth
signature ("batch request must not contain Ironwood spend authorization
signatures"), including the IO-Finalizer sigs on preauthorized padding spends.
Clear them from the request; the wallet-owned base keeps them for extraction
and apply_batch_sig_result adds the device's real-spend signatures on top.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit cd6982bc96ee35bbebbace424078db4ed6c4b3e9)
The Official Ledger app signer (Ironwood/v6 PCZT) only reached the device through the desktop USB HID transport, which does not exist on a phone: there the BLE or USB connection is owned by the Flutter side. Split the transport so the protocol code (Official app, APDU framing) builds on every target, keep hidapi and the Zondax app behind the `ledger` feature, and add a Device that hands each APDU to a Dart closure. Expose it through api::ledger: app version probe, UFVK export, and PCZT signing with progress events. Official accounts can now also be created from a UFVK the host already exported, since the mobile wallet cannot let Rust talk to the device while creating the account. Nym is no longer a default feature: the wallet builds that consume this crate do not ship the mixnet transport, and is_valid_nym_url stays exported so the generated bindings do not depend on the feature. Bindings regenerated with flutter_rust_bridge 2.12.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The flag came with the Cake patch for the ZSA git branch of librustzcash. The crates.io releases this branch builds against reach Ironwood without it, and enabling unstable NU7 code paths in a production wallet build is not something to do by accident. Release builds stay forced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both came in with the external-signer work as scratch probes. The encoding dump wrote v1/v2 encodings to /tmp for inspection and no longer compiles against pczt 0.9. The cross-version check parsed a Cupcake PCZT directly and fails (DeserializeBadOption: the fork's Ironwood/ZSA fields are not the crates.io layout), which is exactly the incompatibility the Keystone wire translation and the signatures-only replies were introduced to avoid; those paths are covered by the keystone_wire unit tests with committed fixtures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three commits on that branch (the upstream 6.24.0 prerelease config, the earlier ZSA re-enable, and the Cake wallet patch) are already represented here: the Cake patch was rebased onto upstream 6.30.0 as "cake wallet patch", and 6.30.0 carries upstream's later ZSA work that superseded the earlier re-enable. Keeps this tree, so the pull request against that branch has it as an ancestor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The move to upstream zkool 6.30.0 changed the pczt this crate pins, and its v2 encoding with it: optional anchors, cv_net, nullifier, rk and cmx; a split-note seed and an asset on every Orchard/Ironwood spend and output; a ZSA note version; a trailing issuance bundle. The wire mirrors still decoded Cake's PCZTs with the old layout, so every airgapped path (Keystone single and batch, Cupcake) would misread or reject a PCZT built today. The tests kept passing only because they replay a fixture recorded before the upgrade. There are now three dialects: `cake` (a new mirror of the pinned encoding, verified byte for byte against a freshly built PCZT), `dst` (Keystone 0.8.0-rc.1, unchanged) and `src_` (the lrz 0.7 fork, which is what the Cupcake signer still parses). Translations go from Cake's dialect to each device and back: - to_keystone / from_keystone, apply_signatures, to_batch_request and apply_batch_sig_result now read and write Cake's dialect; - new to_cupcake / from_cupcake (exposed as pczt_to_cupcake and pczt_from_cupcake), since Cupcake can no longer read Cake's own bytes; - prove_and_finalize accepts a Cupcake-dialect PCZT and translates it. Nothing a device reviews is silently dropped: a spend or output carrying any asset other than ZEC, a split note, a ZSA note version or an issuance bundle is refused. The ZEC asset id the library writes on plain ZEC notes is restored on the way back, and an empty Sapling bundle (present without an anchor in the new encoding, absent in the old) maps between the two. Tests build a real shield PCZT with the pinned library and check it round trips losslessly through both devices, that the Keystone golden bytes (verified against the firmware's parser) still come out of the recorded transaction, that the pinned library parses every translated and merged result, and that a non-ZEC asset is refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audit follow-ups on the Official app path: - The PCZT_HEADER carried a hardcoded "no fallback lock time", nothing modifiable and the network's coin type rather than the PCZT's. Harmless while zkool never sets a lock time, but a future one would have produced signatures that fail verification. The fields are now read from the PCZT (the pinned pczt has no getters for them, so through the wire mirror); a coin type that disagrees with the account's network is refused, and a zero lock time is still sent as absent, so today's transactions produce exactly the header bytes the device has been signing (pinned by a test). - get_ufvk looped forever if the device answered a continuation request with success and no data. That is now a protocol error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…review The progress event was sent before the transaction was streamed, so the host told the user to look at the device several seconds before it drew anything; the device's review appears only after the last packet. The streaming is now reported as "Sending to Ledger" and the confirm event follows the final packet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n be checked A review of the pairing flow pointed out that the viewing key crosses the BLE/USB link unauthenticated and the host imported whatever came back: a substituted key would have put every receive address in an attacker's hands with nothing for the user to notice. The device's own screen is the one part of the path a tampered link cannot alter. GET_SHIELD_ADDR (0x51) with display asks the device to derive the account's unified address (Orchard account path plus m/44'/coin'/account'/0/0) and show it for approval; ledger_show_address exposes that to the host, which displays the address it derived from the imported key alongside so the user can compare them. A refusal, a truncated reply, or anything that is not a unified address with an Orchard receiver is an error. Also refuses a transparent signature reply with no DER bytes instead of indexing into it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
"Signing on Ledger" was announced before the first signing command, which
is the command that blocks until the user approves on the device; a host
following the events told the user the transaction was being signed while
the device was still waiting for them. Progress is now reported after each
signature comes back ("Signed ... n/m"), so "Confirm on your Ledger" stays
current until the user has acted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er its reply The device puts its review on screen when the packet carrying P2_FINISHED arrives and answers that packet only once the user has approved (a real send showed a 17 s gap before the reply and a signature 1 s after it). So the confirm event, sent after the reply, came after the user had already approved. It is now sent just before that packet, and the reply is what marks the approval. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
macsrock
force-pushed
the
ledger-official
branch
from
September 17, 2026 00:39
d2fa4e3 to
8083636
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.
Summary
Brings this fork to upstream hhanh00/zkool2 v6.30.0, which added the official Ledger Zcash app signer (Ironwood/v6 PCZT,
rust/src/ledger/official_sign.rs), re-applies the Cake and Keystone patches on top, and makes the Ledger protocol usable from a phone.Companion to the Cake Wallet pull request adding shielded Zcash over Ledger: cake-tech/cake_wallet#3633 (which also needs cake-tech/ledger-flutter-plus#1).
Changes on top of upstream 6.30.0
keystone-batchrebased: the Cake wallet patch, prove-and-finalize for externally signed PCZTs, external-signer input identification, the Keystone wire dialect and batch signing, transaction fee exposure.ledgerfeature. Newledger::dart_device::DartDevicehands each APDU to a Dart closure (DartFnFuture), andapi::ledgerexposesledger_app_version,ledger_get_ufvk,ufvk_default_addressandledger_sign_transaction(progress + result over a stream). Official accounts (hw = 2) can be created from a UFVK the host already exported.is_valid_nym_urlstays exported so the bindings do not depend on it).zcash_unstable="nu7"; upstream removed that hack and the crates.io releases reach Ironwood without it. Release builds stay forced.Testing
cargo test --lib ledger::(8 new tests: APDU framing, version probe parsing, UFVK chunk reassembly, refusal handling) andcargo test --test ledger_accounts_test(5) pass.aarch64-apple-ios.Note: targeted at
cyjan-2026-08-08, the branch Cake Wallet currently pins (36e72b53, upstream 6.24.0 + the Cake patch);mainshares no history with upstream. The diff is large because it includes upstream 6.24.0 to 6.30.0; the fork-specific changes are the commits afterchore(main): release zkool 6.30.0.Audit follow-ups (added after an external review)
keystone_wire: speak the upgraded pczt encoding…). The upgrade to upstream 6.30.0 changed the pinned pczt's v2 wire layout, so the wire mirrors could no longer read PCZTs Cake builds; the old tests passed only by replaying a pre-upgrade fixture. There are now three mirrors:cake(the pinned encoding, verified byte for byte against a freshly built PCZT),dst(Keystone 0.8.0-rc.1) andsrc_(lrz 0.7, which the Cupcake signer still parses). Newpczt_to_cupcake/pczt_from_cupcake;prove_and_finalizealso accepts a Cupcake-dialect PCZT. Non-ZEC assets, split notes, ZSA note versions and issuance bundles are refused rather than dropped. Tests round-trip a real shield PCZT through both devices, keep the Keystone golden bytes, and parse every translated result with the pinned library.PendingZcashTransaction.commitUR, send CupcakepcztToCupcake(txPlan.pczt)instead of the raw bytes (Keystone already goes throughpcztToKeystone/ the batch request, whose APIs are unchanged).ledger: send the PCZT's own header fields…): lock time, coin type and modifiable flags now come from the PCZT; a coin type that disagrees with the account's network is refused; a zero lock time is still sent as absent, so today's transactions produce the exact header bytes already signed on a device (pinned by a test).Full suite: 165 library tests plus
ledger_accounts_test(5),parse_shield_tx(1),zsa_transfer_test(8); iOS target type-checks.aeb698d0): "Confirm on your Ledger" was emitted before the transaction was streamed, several seconds before the device could show anything; it now follows the last packet, and the streaming is reported as "Sending to Ledger".Second security review (Seth's panel)
Its P1 finding: the exported UFVK crosses the BLE/USB link unauthenticated and was imported as-is, so a key substituted on the link would silently redirect every deposit. Fixed in
d34590e0:get_shield_address(GET_SHIELD_ADDR 0x51 with display, Orchard account path + m/44'/coin'/account'/0/0) has the device derive the account's unified address and show it on its own screen;ledger_show_addressexposes it to the host, which shows the address derived from the imported key alongside so the user compares the two (see the Cake PR). Truncated, non-unified or Orchard-less replies and a refusal are errors. Its two P3s were already fixed here (empty GET_VK continuation chunk) or are now (empty DER in a transparent signature reply).d2fa4e39, amended): a logged send showed the device answers the P2_FINISHED packet only after the user approves (17 s gap, signature 1 s later), so "Confirm on your Ledger" is now emitted just before that packet is sent and the reply marks the approval.67a2513e): "Signing on Ledger" was emitted before the first signing command, i.e. while the device was still waiting for approval; progress is now "Signed … n/m" after each signature returns, so "Confirm on your Ledger" stays current until the user acts.🤖 Generated with Claude Code