# For Devs, How to integrate Zcash-shielded into your wallet ![Screenshot 2024-03-25 at 22.03.15](https://hackmd.io/_uploads/rkTjUPJkA.png) ## Intro - The new Zcash Shielded Application is set to become officially available to the public shortly. - While numerous wallets support Zcash, currently, there is no support for Ledger devices for this new app besides the [development zec wallet fork from zondax](https://github.com/Zondax/zecwallet-lite/) - This article provides a comprehensive overview of the essential steps required to integrate support for the Ledger Zcash Shielded App into your wallet. - This initiative is expected to benefit a wide range of wallets including Ledger Live, Zingo, YWallet, Zashi (new), Flexa, Nighthawk, Unstoppable etc. ## First thing first: In order to integrate the Ledger Zcash-shielded application into your wallet there aren’t magical recipes but we’ll make an exhaustive list of all the available resources and some general guidelines to achieve this. You’ll need to establish communication with the device. To achieve this, Zondax provides 2 communication packages: - [Rust](https://github.com/Zondax/ledger-zcash-rs) communication package - [Javascript](https://www.npmjs.com/package/@zondax/ledger-zcash) communication package Those packages provide the abstraction for establishing communication with your device Using a protocol called APDU (the same protocol as SIM cards). This implies that the zcash application running on the ledger device cannot initiate communication but just respond to commands. To get the updated list of commands that the app will understand, you can check it on the [APDU](https://github.com/Zondax/ledger-zcash/blob/main/docs/APDUSPEC.md) spec documentation You have one extra thing to have in consideration when working with Ledger devices: **private keys / highly sensitive information** won’t be shared with any external party. This means that the different keys that are part of either Transparent/Sapling protocols will be computed and used on the device. You won’t have direct access to them, This means that any signing process or revealing information regarding shielded transactions must be done in the device itself. ## Integrating Zcash-shielded app ### Key management integration guidelines In order to integrate our Ledger application, first you need to design a common interface that handles key management, which would provide access to view/public keys and also useful methods for signing transactions or any other operation that requires access to private keys. A common interface like this would be called a Keystore component that would integrate your current key management system in your wallet that generates keys probably in-memory, and the Ledger application. The first step is to abstract both elements into one type that generalizes over methods for handling and using keys, in the case of Rust, it is suggested to use a sum type like Enum that holds both variants. You can check how this was implemented by Zondax in **ZecWallet** where we added an enum type that abstracts the in-memory [Keystore](https://github.com/Zondax/zecwallet-light-cli/blob/main/lib/src/lightwallet/keys/adapters.rs#L44-L50) and the [LedgerKeystore](https://github.com/Zondax/zecwallet-light-cli/blob/main/lib/src/lightwallet/keys/ledger.rs#L80) component while providing a common set of methods that the rest of your wallet would use for dealing with keys, either requesting view/public keys or signing transactions. Keep in mind that the common interface must align with the security of the Ledger application which will not allow any spending/private key to be sent out of the device. That means you can not keep any sensitive information in-memory in your wallet implementation, if your current design relies on this, you must change it to align with this requirement. At the moment, Zondax is collaborating with Zingolabs to add support for the app. Even though the integration is not completed, you can check how the approach was there for [Keystore](https://github.com/zingolabs/zingolib/blob/3de1d22fc78386cfa2b6015f069b3dff7f7bafcd/zingolib/src/wallet/keys/keystore.rs#L22) abstraction and [LedgerKeystore](https://github.com/zingolabs/zingolib/blob/3de1d22fc78386cfa2b6015f069b3dff7f7bafcd/zingolib/src/wallet/keys/ledger.rs) component. ### Signing Integration guideline This functionality must be centralized as well by the Keystore component. The reason is that as mentioned in the previous sections, private keys must not be kept in memory so that the ledger application won't let them be moved out of the device, because this is a strong requirement, both keystore implementations must align in this regard. This implies that the common interface would provide a method for revealing/signing data using the private keys. For example in our ZecWallet integration, the Keystore integration provides methods for creating node commitments, signing transactions, and many more that require the usage of private keys. ```rust /// Compute the note nullifier pub async fn get_note_nullifier(&self, ivk: &SaplingIvk, position: u64, note: &Note) -> Result<Nullifier, String> { match self { Self::Memory(this) => { let extfvk = this .get_all_extfvks() .into_iter() .find(|extfvk| extfvk.fvk.vk.ivk().to_repr() == ivk.to_repr()) .ok_or(format!("Error: unknown key"))?; Ok(note.nf(&extfvk.fvk.vk, position)) } #[cfg(feature = "ledger-support")] Self::Ledger(this) => { let commitment = Self::compute_note_commitment(note); let commitment: &jubjub::ExtendedPoint = (&commitment).into(); this.compute_nullifier(ivk, position, AffinePoint::from(commitment)) .await .map_err(|e| format!("Error: unable to compute note nullifier: {:?}", e)) } } } ``` You can see how we check for the current Keystore variant being used, and call the corresponding handler. It is important to mention that in the zecwallet integration, only one instance could be used at the time. It is relevant to note that the ledger keystore component internally uses the [ZcashApp](https://github.com/Zondax/ledger-zcash-rs/blob/main/ledger-zcash/src/app.rs#L150) abstraction provided by our [ledger-zcash-rs](https://github.com/Zondax/ledger-zcash-rs/tree/main) library. The ZcashApp component provides a defined set of methods that at the moment we consider sufficient for transparent and sapling operations within zcash protocol. This means that the Orchard and unified addresses protocols are not supported yet. In the following section, we will describe some of the supported operations this component provides and how to use it for establishing communication with the device. A further list of available methods can be found in the source code [here](https://github.com/Zondax/ledger-zcash-rs/blob/main/ledger-zcash/src/app.rs#L399-L1150). ### Basic commands Before you can start sending commands to the app, you need to open the communication channel. This can be done using [Ledger transport](https://www.npmjs.com/package/@ledgerhq/hw-transport). This is just a generic interface for communicating with a Ledger hardware device. There are different kinds of transports based on the technology (channels like U2F, HID, Bluetooth, Webusb) and environment (Node, Web,...) So.. let’s start with simple commands like `GET_DEVICE_INFO` or `GET_VERSION` since they don’t need any additional parameters. You can check some tests that will be really helpful examples: ```rust #[tokio::test] #[serial] async fn version() { init_logging(); log::info!("Test"); let app = ZcashApp::new(TransportNativeHID::new(&HIDAPI).expect("Unable to create transport")); let resp = app.get_version().await.unwrap(); println!("mode {}", resp.mode); println!("major {}", resp.major); println!("minor {}", resp.minor); println!("patch {}", resp.patch); println!("locked {}", resp.locked); assert_eq!(resp.major, 3); } ``` ```javascript test.concurrent.each(models)('get app version', async function (m) { const sim = new Zemu(m.path) try { await sim.start({ ...defaultOptions, model: m.name }) const app = new ZCashApp(sim.getTransport()) const resp = await app.getVersion() console.log(resp) expect(resp.return_code).toEqual(0x9000) expect(resp.error_message).toEqual('No errors') expect(resp).toHaveProperty('test_mode') expect(resp).toHaveProperty('major') expect(resp).toHaveProperty('minor') expect(resp).toHaveProperty('patch') } finally { await sim.close() } }) }) ``` Here you can see the rest of the examples for both [Rust](https://github.com/Zondax/ledger-zcash-rs/blob/main/ledger-zcash/tests/integration_test.rs) and [Javascript](https://github.com/Zondax/ledger-zcash/blob/main/tests_zemu/tests/standard.test.ts). ### Transparent Address To get the transparent address from your device, you’ll need the command `INS_GET_ADDR_SECP256K1` that will need a derivation path. Keep in mind that this is an asynchronous operation and it might fail. This command and any other that doesn’t share sensitive information can be retrieved without explicit user confirmation. Showing the address on the device is a really important confirmation step that the user needs to do before sharing an address. It would be good to add a button or some UI element to allow the user to verify that the wallet and the device show the same address. ```rust #[tokio::test] #[serial] async fn address_unshielded() { init_logging(); let app = ZcashApp::new(TransportNativeHID::new(&HIDAPI).expect("Unable to create transport")); let path = BIP44Path::from_string("m/44'/133'/0'/0/0").unwrap(); let resp = app.get_address_unshielded(&path, false).await.unwrap(); assert_eq!(resp.public_key.len(), PK_LEN_SECP261K1); let pkhex = hex::encode(&resp.public_key[..]); println!("Public Key {:?}", pkhex); println!("Address address {:?}", resp.address); } ``` ```javascript test.concurrent.each(models)('get unshielded address', async function (m) { const sim = new Zemu(m.path) try { await sim.start({ ...defaultOptions, model: m.name }) const app = new ZCashApp(sim.getTransport()) const addr = await app.getAddressAndPubKey("m/44'/133'/5'/0/0", true) console.log(addr) expect(addr.return_code).toEqual(0x9000) const expected_addr_raw = '031f6d238009787c20d5d7becb6b6ad54529fc0a3fd35088e85c2c3966bfec050e' const expected_addr = 't1KHG39uhsssPkYcAXkzZ5Bk2w1rnFukZvx' const addr_raw = addr.address_raw.toString('hex') expect(addr_raw).toEqual(expected_addr_raw) expect(addr.address).toEqual(expected_addr) } finally { await sim.close() } }) ``` ### Sapling keys Within the Sapling upgrade in Zcash, there are several types of keys, each serving different purposes. These keys can be categorized into private and public keys: **Spending key**: this would be like your private key and therefore will never be moved out of the device. Any operation that uses this key will be carried out within the device. **Viewing keys**: even though these can be considered public keys, since they can be used to disclose sensitive information, they will need an explicit approval from the user to be shared with the wallet or any other party. **Nullifiers**: in order to prevent double-spending, Zcash relies on nullifiers that are revealed and recorded in the blockchain when the tokens are spent. These will be needed by the wallets to keep the right balance from your account. Once again, an explicit approval from the user is necessary here. **Payment addresses**: these addresses are are inherently public since the user creates them to shared with someone in order to get a payment. Having one of these addresses doesn't reveal the movements from any other payment address or any of the keys. The Ledger application provides functionalities for retrieving Sapling public keys and addresses. Bellow a short example of the provided API in rust to get the incomming viewing key (IVK): ```rust #[tokio::test] #[serial] async fn get_key_ivk() { init_logging(); let app = ZcashApp::new(TransportNativeHID::new(&HIDAPI).expect("Unable to create transport")); let path = 1000; let resp = app.get_ivk(path).await.unwrap(); let ivk = hex::encode(resp.to_bytes()); assert_eq!( ivk, "6dfadf175921e6fbfa093c8f7c704a0bdb07328474f56c833dfcfa5301082d03" ); } ``` And the next code block demostrates how to use the ZcashApp interface for getting a shielded address from the device: ```rust #[tokio::test] #[serial] async fn address_shielded() { init_logging(); let app = ZcashApp::new(TransportNativeHID::new(&HIDAPI).expect("Unable to create transport")); let path = 1000; let resp = app.get_address_shielded(path, false).await.unwrap(); assert_eq!(resp.public_key.to_bytes().len(), PK_LEN_SAPLING); let pkhex = hex::encode(resp.public_key.to_bytes()); println!("Public Key {:?}", pkhex); println!("Address address {:?}", resp.address); assert_eq!( pkhex, "c69e979c6763c1b09238dc6bd5dcbf35360df95dcadf8c0fa25dcbedaaf6057538b812d06656726ea27667" ); assert_eq!( resp.address, "zs1c60f08r8v0qmpy3cm34ath9lx5mqm72aet0ccrazth97m2hkq46n3wqj6pn9vunw5fmxwclltd3" ); } ``` The device, as mentioned previously, features a limited UI that enables the review of transaction fields and relevant data. The user is required to confirm the accuracy of this data and approve the operation. This verification and approval process will be conducted by the user each time sensitive information needs to leave the device. ```rust // Reviewing our shielded address on device screen #[tokio::test] #[serial] async fn show_address_shielded() { init_logging(); let app = ZcashApp::new(TransportNativeHID::new(&HIDAPI).expect("Unable to create transport")); let path = 1000; let resp = app.get_address_shielded(path, true).await.unwrap(); assert_eq!(resp.public_key.to_bytes().len(), PK_LEN_SAPLING); let pkhex = hex::encode(resp.public_key.to_bytes()); println!("Public Key {:?}", pkhex); println!("Address address {:?}", resp.address); assert_eq!( pkhex, "c69e979c6763c1b09238dc6bd5dcbf35360df95dcadf8c0fa25dcbedaaf6057538b812d06656726ea27667" ); assert_eq!( resp.address, "zs1c60f08r8v0qmpy3cm34ath9lx5mqm72aet0ccrazth97m2hkq46n3wqj6pn9vunw5fmxwclltd3" ); } ``` ### Transaction signing This is one of the most important command that our communication interface provides. The overall idea is that the transaction data is sent serialized to the devices, users would them have to review the transaction and upon user confirmation, it would be signed. The returned data to the wallet would contain the signature. This is the general idea but to integrate the Zcash transaction signing feature of our Ledger application into your wallet, it's crucial to understand that the signing process is not straightforward but rather interactive and step-by-step due to the cryptographic complexities involved and the limited resources in terms of storage, memory and computing power from Ledger devices. Below you can see an overview of the process: 1. **Initialization**: The process begins with gathering transaction input data, including transparent and shielded inputs and outputs. This would depend of the kind of transaction either transparent->shielded, transparent->transparent, shielded->transparent or shielded->shielded transactions. 2. **Building the Transaction**: - **Transparent Inputs**: Add transparent inputs to the transaction builder without needing fresh information from the Ledger. This step requires the public key and outpoint from the blockchain. - **Shielded Spends**: For each shielded spend, the Ledger device provides the proof generation key, value commitment randomness (`rcv`), and randomness for the random verification key (`alpha`). This data is added to the transaction builder. - **Shielded Outputs**: Similar to shielded spends, adding shielded outputs requires randomness for the value commitment (`rcv`), note commitment (`rcm`), and random encryption key (`esk`), provided by the Ledger. This step is repeated for each shielded output. 3. **Finalizing the Transaction**: Once all inputs and outputs are added, the transaction builder compiles the transaction, including Zero-Knowledge (ZK) proofs. This compiled transaction data (`blob_txdata`) is then sent back to the Ledger for validation and signing. 4. **Extracting Signatures**: The final step involves extracting signatures for both shielded spends and any transparent inputs from the Ledger. These signatures are then added to the transaction builder. 5. **Completion**: With all signatures in place, the transaction builder finalizes the transaction, making it ready for broadcast to the Zcash network. This interactive process ensures the security and integrity of the transaction by leveraging the Ledger's cryptographic capabilities. It's essential for wallet developers to implement this step-by-step approach to integrate Zcash transaction signing functionality successfully, for further details you can look at our transaction [builder](https://github.com/Zondax/ledger-zcash-rs/blob/main/ledger-zcash/src/txbuilder.rs#L30) component in Rust and some [tests](https://github.com/Zondax/ledger-zcash-rs/blob/main/ledger-zcash/tests/integration_test.rs#L275-L538) on how it is used to build a transactions. If you are integrating this Ledger application in `Javascript/Typescript`, you can take a look at our [tests](https://github.com/Zondax/ledger-zcash/blob/d72efa5e52caf98d8471bb3f45e11f487a74c8fa/tests_zemu/tests/zcashtool.test.ts#L1413) that provides a good insight on how perform this interactive process. -------------- ## Limitations: At the moment, there are some features that are not supported by the application and have to be considered when you plan to integrate this app in your wallet. ### UTXO limitaion Due to the very limited resources from the device, the application can handle up to 5 transparent and 5 shielded inputs/outputs. If your transaction involves too many inputs/outputs, the device might not be able to handle it and it will be rejected every time. At the moment, these values are fixed. So a transaction with 6 transparent inputs cannot be processed but a transaction with 5 transparent inputs, 5 shielded inputs, 5 transparent outputs and 5 shielded output would work just fine. ### Unified address After the activation from Network Upgrade 5 (NU5), the default address type is the Unified Address (UA). At the moment, the app is not able to generate these types of addresses, so you'll need to consider this while making the integration. ### Orchard Orchard protocol is not supported yet by the application. Consider that the application can only process Sapling transactions and you might need to adapt the wallet for it ### Generic communication package The communication package is highly coupled with types that were used by ZecWallet. Therefore, it might not fit well or it might need some changes. Please, don't hesitate to open an issue in [Zcash app](https://github.com/Zondax/ledger-zcash-rs/tree/main) or Rust comm package repositories