# Zcash Version Negotiation This document describes how Zcash wallets and Keystone hardware wallets exchange firmware version information, enabling wallets to gate features based on device capabilities. ## Overview Today the keystone does not report its firmware version to the wallet. We update Keystone to reports its firmware version to wallets through two independent signals: 1. **At pairing time** via the `ZcashAccounts` QR code (a `deviceVersion` field in the CBOR payload) 2. **On every signed transaction** via `global.proprietary["keystone:fw_version"]` in the signed PCZT Both signals are optional and backwards compatible. Old firmware omits them, and old wallets ignore them. ## Signal 1: Pairing QR (`ZcashAccounts`) When a user connects their Keystone to a wallet by scanning the accounts QR code, the `ZcashAccounts` payload now includes an optional `deviceVersion` field. The QR uses the [Uniform Resource (UR)](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md) encoding, which wraps [CBOR (Concise Binary Object Representation)](https://cbor.io/) data into animated multi-frame QR codes for camera-based transmission. ### CBOR Map | Key | Field | Type | Required | | --- | ------------------ | ----- | -------- | | 1 | `seed_fingerprint` | bytes | Yes | | 2 | `accounts` | array | Yes | | 3 | `device_version` | text | No | This mirrors the existing `CryptoMultiAccounts` type used by other chains (Ethereum, Solana, etc.), which already carries `device`, `deviceId`, and `deviceVersion` fields at CBOR keys 3, 4, and 5 respectively. TODO: Kris do you have an opinion of if we should add all 5. ### Example A Keystone running firmware 12.4.0 produces: ``` { 1: h'd1d1d1...d1d1d1', // seed_fingerprint 2: [40001(...)], // accounts (tagged UFVKs) 3: "2.4.0-cypherpunk" // device_version } ``` ### Backwards Compatibility | Scenario | Behavior | | ------------------------- | --------------------------------------------------------------- | | New Keystone + old wallet | Wallet ignores unknown CBOR key 3. Pairing works normally. | | Old Keystone + new wallet | Key 3 is absent. Wallet sees `nil` and treats device as legacy. | ## Signal 2: Signed PCZT (`global.proprietary`) Every signed PCZT response from Keystone carries a firmware version stamp in the PCZT's `global.proprietary` map. ### Proprietary Entry | Key | Value | Encoding | | --------------------- | ----------------------- | ----------- | | `keystone:fw_version` | `[major, minor, build]` | 3 raw bytes | The version triple matches `SOFTWARE_VERSION_MAJOR`, `SOFTWARE_VERSION_MINOR`, and `SOFTWARE_VERSION_BUILD` from the firmware's `version.h`. TODO: Check on non cipherpunk ### Signing Flow ``` Wallet Keystone | | | ---- unsigned PCZT (QR) -------> | | | signs transaction | | stamps global.proprietary["keystone:fw_version"] | | redacts witness data | <--- signed PCZT (QR) ---------- | | | | reads fw_version from response | | caches / evaluates policy | ``` The stamp is applied via the PCZT `Updater` role after signing but before the `Redactor` strips witness data. The `Redactor` does not touch `global`, so the stamp survives into the returned bytes. ### Backwards Compatibility | Scenario | Behavior | | ------------------------- | ----------------------------------------------------------------------- | | New Keystone + old wallet | Wallet ignores unknown proprietary keys. Transaction proceeds normally. | | Old Keystone + new wallet | `keystone:fw_version` is absent. Wallet treats device as legacy. | ## Wallet Behavior The firmware does not enforce any version policy. It reports its version; the wallet decides what to do with it. ### Caching Wallets should cache the detected firmware version, keyed by the device's `seedFingerprint`: - **On pairing:** read `deviceVersion` from the `ZcashAccounts` QR and cache it - **On each signed transaction:** read `keystone:fw_version` from the PCZT response and update the cache (detects firmware upgrades between pairing and signing) Once cached, the wallet knows the device's firmware version without needing another round-trip. ### Future: Quantum-Recoverable Note Gating When quantum-recoverable (QR) notes are introduced to the Zcash protocol, the cached firmware version enables wallets to gate QR-note spends on firmware capability: - **Transaction does not spend QR notes:** proceed regardless of firmware version. Optionally warn if the device is legacy. - **Transaction spends QR notes:** require firmware at or above the QR-capable version. Block the transaction if the device is too old or unknown. The assumption is that firmware versions that support version negotiation will also support QR note signing. ## PRs ### QR connection passing firmware version - `device_version` in `ZcashAccounts` CBOR: https://github.com/KeystoneHQ/keystone-sdk-rust/pull/127 - `deviceVersion` in Swift model: https://github.com/KeystoneHQ/keystone-sdk-ios/pull/39 - Firmware passes version in pairing QR: https://github.com/KeystoneHQ/keystone3-firmware/pull/2138 ### Firmware version passed with signed PCZT - Firmware version stamp on signed PCZTs: https://github.com/KeystoneHQ/keystone3-firmware/pull/2134 ## Figjam Diagram A basic [figjam flow chart](https://www.figma.com/board/DNYPvFwUqO94u9JgRwlU7T/Keystone-Wallet-Version-Negotiation?node-id=1-2&t=hLD1Tw9qTeSSddV2-1) that is a high level overview of what was discussed above: ![image](https://hackmd.io/_uploads/S1i2UxRh-x.png) ## Figma Design A rough draft of the above flow chart designed for zodl: https://www.figma.com/design/XraEBDVrZO3wEMx6qzQhOM/Draft-QR-Flows?node-id=0-1&t=OctuEZs2drwjjWxs-1 ## FAQ ### Why no build-flavor field? Flavor is deliberately omitted. A firmware that can emit the pairing UR or a signed PCZT must already have app_zcash compiled in, so the artifact's existence is itself the capability signal. An explicit variant byte would be a redundant check (i.e. to confirm we are on the Cipherpunk firmware varient)