From ede3e58a393afd19bdcae3beca555fdae1bcf2cc Mon Sep 17 00:00:00 2001 From: Antoine Serval Date: Tue, 14 Apr 2026 07:27:36 +0200 Subject: [PATCH] feat: add Zcash chain support with PCZT signing and unified addresses Add Zcash as the first privacy-preserving chain in OWS, with full shielded transaction support via the PCZT format. Chain registration: - ChainType::Zcash with CAIP-2 namespace, coin type 133 - Default lightwalletd endpoints (zec.rocks mainnet/testnet) Wallet creation (ows wallet create): - ZIP-32 key derivation from BIP-39 seed (needs_raw_seed trait extension) - Unified address with Orchard + Sapling receivers, shielded by default PCZT signing (ows sign tx --chain zcash): - Orchard spend authorization (RedPallas) - Sapling spend authorization (Jubjub) - Transparent spend authorization (secp256k1) - Skips non-matching/dummy actions per standard Orchard builder behavior - Key material zeroized after use Sign + broadcast (ows sign send-tx --chain zcash): - PCZT finalization via TransactionExtractor - Broadcast via lightwalletd gRPC with TLS Feature-gated behind zcash-shielded (enabled by default in CLI). Without the feature, falls back to transparent-only t-address support. Dependencies: zcash_keys, pczt, orchard, sapling-crypto, zcash_transparent, zcash_protocol, zcash_primitives (all from librustzcash, maintained by ZODL). Tests: 27 Zcash-specific tests (15 signer unit tests + 12 library integration tests). All 465 workspace tests pass. Docs: zcash-guide.md covering wallet creation, PCZT pipeline, security model, configuration, and end-to-end workflow. --- CONTRIBUTING.md | 24 +- docs/07-supported-chains.md | 10 +- docs/zcash-guide.md | 197 +++ ows/Cargo.lock | 1199 ++++++++++++++++- ows/crates/ows-cli/Cargo.toml | 4 + ows/crates/ows-cli/src/commands/derive.rs | 36 +- .../ows-cli/src/commands/send_transaction.rs | 35 +- .../ows-cli/src/commands/sign_message.rs | 10 +- .../ows-cli/src/commands/sign_transaction.rs | 8 + ows/crates/ows-core/src/chain.rs | 20 +- ows/crates/ows-core/src/config.rs | 10 +- ows/crates/ows-lib/Cargo.toml | 3 + ows/crates/ows-lib/src/lib.rs | 2 + ows/crates/ows-lib/src/lwd_grpc.rs | 84 ++ ows/crates/ows-lib/src/ops.rs | 381 +++++- ows/crates/ows-signer/Cargo.toml | 10 + ows/crates/ows-signer/src/chains/mod.rs | 3 + ows/crates/ows-signer/src/chains/zcash.rs | 648 +++++++++ ows/crates/ows-signer/src/traits.rs | 26 + 19 files changed, 2630 insertions(+), 80 deletions(-) create mode 100644 docs/zcash-guide.md create mode 100644 ows/crates/ows-lib/src/lwd_grpc.rs create mode 100644 ows/crates/ows-signer/src/chains/zcash.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a433076a..5b37c3e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,13 +30,35 @@ cd bindings/python && maturin develop --release ### Running Tests ```bash -# Rust tests +# Rust tests (all features, including Zcash shielded support) +cd ows && cargo test --workspace --features zcash-shielded + +# Rust tests (without Zcash shielded — faster, smaller dependency tree) cd ows && cargo test --workspace # Node tests cd bindings/node && npm test ``` +### Zcash Development + +Zcash shielded support (`zcash-shielded` feature) is enabled by default in the CLI but optional in the library crates. When developing Zcash-specific code: + +```bash +# Build with Zcash shielded support +cd ows && cargo build --workspace --features zcash-shielded + +# Run only Zcash tests +cd ows && cargo test --features zcash-shielded -- zcash + +# Clippy with Zcash features +cd ows && cargo clippy --workspace --features zcash-shielded -- -D warnings +``` + +The `zcash-shielded` feature adds dependencies on `zcash_keys`, `pczt`, `orchard`, and `sapling-crypto` from the [librustzcash](https://github.com/zcash/librustzcash) ecosystem. These are heavier than the base OWS dependencies — expect longer initial compile times. + +See [docs/zcash-guide.md](docs/zcash-guide.md) for architecture and usage details. + ### Code Formatting ```bash diff --git a/docs/07-supported-chains.md b/docs/07-supported-chains.md index 6a2961d6..a66ed9b6 100644 --- a/docs/07-supported-chains.md +++ b/docs/07-supported-chains.md @@ -21,7 +21,7 @@ type AssetId = `${ChainId}:${string}`; // e.g. "eip155:8453:native" (ETH on Base) ``` -The `native` token refers to the chain's native currency (ETH, SOL, SUI, BTC, ATOM, TRX, TON, etc.). +The `native` token refers to the chain's native currency (ETH, SOL, SUI, BTC, ATOM, TRX, TON, ZEC, etc.). ## Chain Families @@ -38,6 +38,7 @@ OWS groups chains into families that share a cryptographic curve and address der | Sui | ed25519 | 784 | `m/44'/784'/{index}'/0'/0'` | `0x` + BLAKE2b-256 hex (32 bytes) | `sui` | | Spark | secp256k1 | 8797555 | `m/84'/0'/0'/0/{index}` | `spark:` + compressed pubkey hex | `spark` | | Filecoin | secp256k1 | 461 | `m/44'/461'/0'/0/{index}` | `f1` + base32(blake2b-160) | `fil` | +| Zcash | secp256k1 / ZIP-32 | 133 | ZIP-32 (from raw seed) | Unified address (`u1...`) | `zcash` | ## Known Networks @@ -67,6 +68,7 @@ Each network has a canonical chain identifier. Endpoint discovery and transport | Sui | `sui:mainnet` | | Spark | `spark:mainnet` | | Filecoin | `fil:mainnet` | +| Zcash | `zcash:mainnet` | Implementations MAY ship convenience endpoint defaults, but those defaults are deployment choices rather than OWS interoperability requirements. @@ -90,6 +92,7 @@ ton → ton:mainnet sui → sui:mainnet spark → spark:mainnet filecoin → fil:mainnet +zcash → zcash:mainnet ``` Aliases MUST be resolved to full CAIP-2 identifiers before any processing. They MUST NOT appear in wallet files, policy files, or audit logs. @@ -112,11 +115,14 @@ Master Seed (512 bits via PBKDF2) ├── m/44'/607'/0' → TON Account 0 ├── m/44'/784'/0'/0'/0' → Sui Account 0 ├── m/84'/0'/0'/0/0 → Spark Account 0 - └── m/44'/461'/0'/0/0 → Filecoin Account 0 + ├── m/44'/461'/0'/0/0 → Filecoin Account 0 + └── ZIP-32(seed, 0) → Zcash Account 0 (unified address) ``` A single mnemonic derives accounts across all supported chains. The wallet file stores the encrypted mnemonic; the signer derives the appropriate private key using each chain's coin type and derivation path. +> **Note on Zcash:** Zcash unified addresses use ZIP-32 derivation, which operates on the raw BIP-39 seed rather than a BIP-32 derived key. The signer signals this via `needs_raw_seed() → true`, and the wallet layer passes the full 64-byte seed instead of a 32-byte derived key. See [Zcash Integration Guide](zcash-guide.md) for details. + ## Adding a New Chain 1. Define a canonical chain identifier, preferably using CAIP-2. diff --git a/docs/zcash-guide.md b/docs/zcash-guide.md new file mode 100644 index 00000000..a0e98b6c --- /dev/null +++ b/docs/zcash-guide.md @@ -0,0 +1,197 @@ +# Zcash Integration Guide + +Zcash is the first privacy-preserving chain in OWS. It supports shielded transactions via the PCZT (Partially Created Zcash Transaction) format, giving agents and wallets a way to hold and spend ZEC without exposing transaction metadata. + +## How Zcash differs from other OWS chains + +Every other chain in OWS follows the same pattern: derive a key via BIP-44, hash and sign a transaction, broadcast via JSON-RPC. Zcash breaks this pattern in two ways: + +1. **Key derivation uses ZIP-32, not BIP-44.** Zcash unified addresses contain Orchard and Sapling receivers, which require the raw BIP-39 seed passed through the ZIP-32 derivation scheme — not a BIP-32 derived private key. + +2. **Signing operates on PCZTs, not raw transactions.** Zcash shielded transactions require zero-knowledge proofs and per-pool spend authorization signatures (RedPallas for Orchard, Jubjub for Sapling). OWS handles only the signing step. The ZK proof generation happens externally. + +## Wallet creation + +```bash +ows wallet create --name my-wallet +``` + +Zcash appears alongside all other chains. The derived address is a unified address (`u1...`) with Orchard and Sapling receivers — shielded by default. + +``` +eip155:1 → 0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb +solana:... → 7v91N7iZ9mNicL8WfG6cgSCKyRXydQjLh6UYBWwm6y1Q +zcash:mainnet → u1qxnwj8h... +``` + +Under the hood, OWS detects that the Zcash signer requires the raw seed (`needs_raw_seed() → true`) and passes the full 64-byte BIP-39 seed through ZIP-32 derivation instead of BIP-44: + +``` +BIP-39 Mnemonic + │ + ▼ +Raw Seed (64 bytes) + │ + ├── BIP-44 path → EVM, Solana, Bitcoin, ... + │ + └── ZIP-32 → Unified Spending Key + │ + ├── Orchard receiver + ├── Sapling receiver + └── Unified Address (u1...) +``` + +## Address derivation + +To derive a Zcash address from an existing mnemonic: + +```bash +echo "your twelve word mnemonic ..." | ows derive --chain zcash +``` + +This returns a unified address. To derive all chains at once: + +```bash +echo "your twelve word mnemonic ..." | ows derive +``` + +## Signing a PCZT + +### The PCZT pipeline + +Zcash transactions are built collaboratively using the PCZT format. The roles are: + +``` +Creator → Builds the transaction structure (inputs, outputs, amounts) + │ +Prover → Generates zero-knowledge proofs (Sapling, Orchard) + │ +Signer (OWS) → Applies spend authorization signatures + │ +Finalizer → Extracts the signed transaction + │ +Broadcaster → Sends to the network via lightwalletd +``` + +OWS fills the **Signer** role. It receives a PCZT that already has ZK proofs applied, decrypts the spending key from the vault, signs the relevant inputs, and returns the signed PCZT. + +### CLI usage + +```bash +# Sign a PCZT (hex-encoded) +ows sign tx --chain zcash --wallet my-wallet --tx +``` + +The `` must be a serialized PCZT that has already been through the Creator and Prover stages. Tools that produce PCZTs include: + +- **zipher-cli** (`zipher create-pczt`) +- **Zodl** (the Zcash reference wallet, via `zcash_client_backend`) +- Any tool using `zcash_client_backend::data_api::wallet::create_pczt_from_proposal` + +The output is the signed PCZT in hex. The caller is responsible for finalization and broadcast. + +### Security: trusting the PCZT source + +OWS signs whatever PCZT is passed to it — the same trust model as every other chain. When you call `ows sign tx --chain evm`, OWS signs the unsigned EVM transaction without inspecting whether the recipient or amount is what you intended. The caller (your agent, your CLI, your wallet) is responsible for constructing a safe transaction. + +For Zcash, this means: if a malicious PCZT is constructed to send your funds to an attacker's address, OWS will sign it. The PCZT format does carry metadata (amounts, recipients, memos) that a higher-level application could inspect before requesting a signature, but OWS itself operates at the signing primitive level — it does not enforce spending policies at the PCZT layer. + +This is consistent with the OWS security model: the vault holds keys, the signer signs, and the policy engine (if configured) enforces spending limits. Transaction construction and validation are the caller's responsibility across all chains. + +### Sign and broadcast + +For end-to-end sending: + +```bash +ows sign send-tx --chain zcash --wallet my-wallet --tx +``` + +This signs the PCZT, extracts the finalized transaction, and broadcasts it to lightwalletd via gRPC. The default endpoint is `zec.rocks:443` for mainnet. + +Returns the transaction ID (txid) on success. + +## What OWS signs + +When `sign tx --chain zcash` is called, OWS: + +1. Decrypts the BIP-39 mnemonic from the vault +2. Derives the Unified Spending Key via ZIP-32 +3. Extracts per-pool signing keys: + - **Orchard:** `SpendAuthorizingKey` (RedPallas) + - **Sapling:** `ask` (Jubjub) + - **Transparent:** secp256k1 secret key +4. Iterates over PCZT inputs and signs those matching the derived keys +5. Skips dummy/padding actions (standard Orchard behavior) +6. Returns the signed PCZT + +The spending key is zeroized after use. + +## Configuration + +### Default RPC endpoints + +| Network | Endpoint | +|---------|----------| +| `zcash:mainnet` | `https://zec.rocks:443` | +| `zcash:testnet` | `https://testnet.zec.rocks:443` | + +Override via `~/.ows/config.toml`: + +```toml +[rpc] +"zcash:mainnet" = "https://your-lightwalletd:443" +``` + +Or pass `--rpc-url` on the CLI. + +### Feature flag + +Zcash shielded support requires the `zcash-shielded` feature flag, which is enabled by default in the CLI. The feature adds dependencies on `zcash_keys`, `pczt`, `orchard`, and `sapling-crypto` for ZIP-32 derivation and PCZT signing. + +Without `zcash-shielded`, Zcash falls back to transparent-only support (t-addresses, secp256k1 signing). + +## Dependencies + +| Crate | Purpose | +|-------|---------| +| `zcash_keys` 0.12 | ZIP-32 key derivation, unified address encoding | +| `zcash_protocol` 0.7 | Network parameters (mainnet/testnet) | +| `zip32` 0.2 | Account ID types | +| `pczt` 0.5 | PCZT parsing, Signer role | +| `orchard` 0.11 | Orchard spend authorization key types | +| `sapling-crypto` 0.5 | Sapling spend auth key (`ask`) | +| `zcash_transparent` 0.6 | Transparent key derivation | +| `zcash_primitives` 0.26 | Transaction serialization (for broadcast) | + +All crates are from the official [librustzcash](https://github.com/zcash/librustzcash) ecosystem, originally built by Electric Coin Company and now maintained by [ZODL](https://zodl.com) (Zcash Open Development Lab). + +## End-to-end example + +A complete shielded send using OWS and zipher-cli: + +```bash +# 1. Create an OWS wallet +ows wallet create --name agent-wallet + +# 2. Fund the Zcash address (send ZEC to the u1... address) + +# 3. Build a PCZT with zipher-cli (Creator + Prover) +PCZT=$(zipher create-pczt \ + --to u1recipient... \ + --amount 0.01 \ + --data-dir ~/.zipher) + +# 4. Sign with OWS (Signer) +ows sign send-tx --chain zcash --wallet agent-wallet --tx $PCZT + +# 5. Transaction is broadcast and visible on a block explorer +``` + +## References + +- [ZIP-32: Shielded Hierarchical Deterministic Wallets](https://zips.z.cash/zip-0032) +- [ZIP-316: Unified Addresses](https://zips.z.cash/zip-0316) +- [ZIP-244: Transaction Identifier and Commitment](https://zips.z.cash/zip-0244) +- [PCZT specification](https://github.com/zcash/zips/pull/766) +- [OWS Signing Interface](02-signing-interface.md) +- [OWS Supported Chains](07-supported-chains.md) diff --git a/ows/Cargo.lock b/ows/Cargo.lock index 947d481c..27fae1b9 100644 --- a/ows/Cargo.lock +++ b/ows/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -102,6 +102,18 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-stream" version = "0.3.6" @@ -230,6 +242,43 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +[[package]] +name = "bellman" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afceed28bac7f9f5a508bca8aeeff51cdfa4770c0b967ac55c621e2ddfd6171" +dependencies = [ + "bitvec", + "blake2s_simd", + "byteorder", + "crossbeam-channel", + "ff", + "group", + "lazy_static", + "log", + "num_cpus", + "pairing", + "rand_core 0.6.4", + "rayon", + "subtle", +] + +[[package]] +name = "bip32" +version = "0.6.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143f5327f23168716be068f8e1014ba2ea16a6c91e8777bc8927da7b51e1df1f" +dependencies = [ + "bs58", + "hmac 0.13.0-pre.4", + "rand_core 0.6.4", + "ripemd 0.2.0-pre.4", + "secp256k1", + "sha2 0.11.0-pre.4", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -254,7 +303,29 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", ] [[package]] @@ -266,13 +337,44 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd016a0ddc7cb13661bf5576073ce07330a693f8608a1320b4e20561cc12cdc" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "bounded-vec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dc0086e469182132244e9b8d313a0742e1132da43a08c24b9dd3c18e0faf3a" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "bs58" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2", + "sha2 0.10.9", "tinyvec", ] @@ -282,12 +384,27 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.57" @@ -310,6 +427,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.44" @@ -330,8 +471,9 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", + "zeroize", ] [[package]] @@ -374,6 +516,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "coins-bip32" version = "0.11.1" @@ -382,11 +533,11 @@ checksum = "66c43ff7fd9ff522219058808a259e61423335767b1071d5b346de60d9219657" dependencies = [ "bs58", "coins-core", - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", "k256", "serde", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -398,11 +549,11 @@ checksum = "4c4587c0b4064da887ed39a6522f577267d57e58bdd583178cd877d721b56a2e" dependencies = [ "bitvec", "coins-bip32", - "hmac", + "hmac 0.12.1", "once_cell", "pbkdf2", "rand 0.8.5", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -416,11 +567,11 @@ dependencies = [ "bech32 0.9.1", "bs58", "const-hex", - "digest", + "digest 0.10.7", "generic-array", - "ripemd", + "ripemd 0.1.3", "serde", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", ] @@ -449,12 +600,27 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -464,6 +630,46 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -487,6 +693,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -505,7 +720,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -523,6 +738,40 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.10" @@ -533,15 +782,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" +dependencies = [ + "block-buffer 0.11.0-rc.3", + "crypto-common 0.2.0-rc.1", "subtle", ] @@ -556,6 +825,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -563,7 +841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -589,7 +867,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -608,7 +886,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -619,6 +897,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equihash" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4f333d4ccc9d23c06593733673026efa71a332e028b00f12cf427b9677dce9" +dependencies = [ + "blake2b_simd", + "core2", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -635,6 +935,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "f4jumble" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7" +dependencies = [ + "blake2b_simd", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -647,6 +956,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] @@ -684,6 +994,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fpe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" +dependencies = [ + "cbc", + "cipher", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "funty" version = "2.0.0" @@ -780,6 +1104,18 @@ dependencies = [ "wasip3", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ghash" version = "0.5.1" @@ -797,6 +1133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", + "memuse", "rand_core 0.6.4", "subtle", ] @@ -820,6 +1157,61 @@ dependencies = [ "tracing", ] +[[package]] +name = "halo2_gadgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand 0.8.5", + "sinsemilla", + "subtle", + "uint", +] + +[[package]] +name = "halo2_legacy_pdqsort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" + +[[package]] +name = "halo2_poseidon" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] + +[[package]] +name = "halo2_proofs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05713f117155643ce10975e0bee44a274bcda2f4bb5ef29a999ad67c1fa8d4d3" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_legacy_pdqsort", + "indexmap 1.9.3", + "maybe-rayon", + "pasta_curves", + "rand_core 0.6.4", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -847,6 +1239,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -859,7 +1257,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -868,7 +1266,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" +dependencies = [ + "digest 0.11.0-pre.9", ] [[package]] @@ -916,6 +1323,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -953,7 +1369,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -1103,6 +1519,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1124,6 +1546,15 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "incrementalmerkletree" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30821f91f0fa8660edca547918dc59812893b497d07c1144f326f07fdd94aba9" +dependencies = [ + "either", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1202,6 +1633,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jubjub" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" +dependencies = [ + "bitvec", + "bls12_381", + "ff", + "group", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "k256" version = "0.13.4" @@ -1212,7 +1657,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2", + "sha2 0.10.9", "signature", ] @@ -1225,6 +1670,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1237,6 +1691,12 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1249,6 +1709,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "log" version = "0.4.29" @@ -1267,12 +1733,28 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + [[package]] name = "mime" version = "0.3.17" @@ -1290,6 +1772,37 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nonempty" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1299,6 +1812,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1317,6 +1840,41 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "orchard" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ef66fcf99348242a20d582d7434da381a867df8dc155b3a980eca767c56137" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "core2", + "ff", + "fpe", + "getset", + "group", + "halo2_gadgets", + "halo2_poseidon", + "halo2_proofs", + "hex", + "incrementalmerkletree", + "lazy_static", + "memuse", + "nonempty", + "pasta_curves", + "rand 0.8.5", + "reddsa", + "serde", + "sinsemilla", + "subtle", + "tracing", + "visibility", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + [[package]] name = "ows-cli" version = "1.0.0" @@ -1362,17 +1920,19 @@ dependencies = [ "k256", "ows-core", "ows-signer", + "pczt", "prost", "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sha3", "tempfile", "thiserror 2.0.18", "tokio", "tonic", "uuid", + "zcash_primitives", "zeroize", ] @@ -1402,24 +1962,40 @@ dependencies = [ "bs58", "coins-bip32", "coins-bip39", - "digest", + "digest 0.10.7", "ed25519-dalek", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "k256", "libc", + "orchard", "ows-core", + "pczt", "rand 0.8.5", - "ripemd", + "ripemd 0.1.3", + "sapling-crypto", "scrypt", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sha3", "signal-hook", "thiserror 2.0.18", + "zcash_keys", + "zcash_protocol", + "zcash_transparent", "zeroize", + "zip32", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", ] [[package]] @@ -1433,14 +2009,58 @@ dependencies = [ "subtle", ] +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand 0.8.5", + "static_assertions", + "subtle", +] + [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pczt" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea1cd05607eee33d320860c8e1159e78770f1d1909748c72f53959e106c6899" +dependencies = [ + "blake2b_simd", + "bls12_381", + "document-features", + "ff", + "getset", + "jubjub", + "nonempty", + "orchard", + "pasta_curves", + "postcard", + "rand_core 0.6.4", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "zcash_note_encryption", + "zcash_primitives", + "zcash_protocol", + "zcash_script", + "zcash_transparent", ] [[package]] @@ -1491,6 +2111,17 @@ dependencies = [ "spki", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -1503,6 +2134,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1513,21 +2156,49 @@ dependencies = [ ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "zerocopy", + "proc-macro2", + "quote", ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", + "quote", "syn", ] @@ -1728,6 +2399,56 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reddsa" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a5191930e84973293aa5f532b513404460cd2216c1cfb76d08748c15b40b02" +dependencies = [ + "blake2b_simd", + "byteorder", + "group", + "hex", + "jubjub", + "pasta_curves", + "rand_core 0.6.4", + "serde", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "redjubjub" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b0ac1bc6bb3696d2c6f52cff8fba57238b81da8c0214ee6cd146eb8fde364e" +dependencies = [ + "rand_core 0.6.4", + "reddsa", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "regex-syntax" version = "0.8.10" @@ -1769,7 +2490,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -1778,7 +2499,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -1802,7 +2523,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "ripemd" +version = "0.2.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48cf93482ea998ad1302c42739bc73ab3adc574890c373ec89710e219357579" +dependencies = [ + "digest 0.11.0-pre.9", ] [[package]] @@ -1860,6 +2590,7 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -1868,6 +2599,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -1910,6 +2650,39 @@ dependencies = [ "cipher", ] +[[package]] +name = "sapling-crypto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d3c081c83f1dc87403d9d71a06f52301c0aa9ea4c17da2a3435bbf493ffba4" +dependencies = [ + "aes", + "bellman", + "bitvec", + "blake2b_simd", + "blake2s_simd", + "bls12_381", + "core2", + "document-features", + "ff", + "fpe", + "getset", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "lazy_static", + "memuse", + "rand 0.8.5", + "rand_core 0.6.4", + "redjubjub", + "subtle", + "tracing", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -1919,7 +2692,7 @@ dependencies = [ "password-hash", "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1936,6 +2709,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.27" @@ -1997,6 +2797,44 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2005,7 +2843,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-pre.9", ] [[package]] @@ -2014,7 +2863,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -2050,10 +2899,21 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "sinsemilla" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +dependencies = [ + "group", + "pasta_curves", + "subtle", +] + [[package]] name = "slab" version = "0.4.12" @@ -2086,6 +2946,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -2102,6 +2968,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2204,6 +3076,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.2" @@ -2310,13 +3201,16 @@ dependencies = [ "percent-encoding", "pin-project", "prost", + "rustls-pemfile", "socket2 0.5.10", "tokio", + "tokio-rustls", "tokio-stream", "tower 0.4.13", "tower-layer", "tower-service", "tracing", + "webpki-roots 0.26.11", ] [[package]] @@ -2427,6 +3321,18 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" @@ -2451,7 +3357,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -2502,6 +3408,17 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "want" version = "0.3.1" @@ -2648,6 +3565,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -3007,6 +3933,178 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zcash_address" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4491dddd232de02df42481757054dc19c8bc51cf709cfec58feebfef7c3c9a" +dependencies = [ + "bech32 0.11.1", + "bs58", + "core2", + "f4jumble", + "zcash_encoding", + "zcash_protocol", +] + +[[package]] +name = "zcash_encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca38087e6524e5f51a5b0fb3fc18f36d7b84bf67b2056f494ca0c281590953d" +dependencies = [ + "core2", + "nonempty", +] + +[[package]] +name = "zcash_keys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c115531caa1b7ca5ccd82dc26dbe3ba44b7542e928a3f77cd04abbe3cde4a4f2" +dependencies = [ + "bech32 0.11.1", + "bip32", + "blake2b_simd", + "bls12_381", + "bs58", + "core2", + "document-features", + "group", + "memuse", + "nonempty", + "orchard", + "rand_core 0.6.4", + "sapling-crypto", + "secrecy", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_protocol", + "zcash_transparent", + "zip32", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" +dependencies = [ + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "zcash_primitives" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd9ff256fb298a7e94a73c1adad6c7e0b4b194b902e777ee9f5f2e12c4c4776" +dependencies = [ + "bip32", + "blake2b_simd", + "block-buffer 0.11.0-rc.3", + "bs58", + "core2", + "crypto-common 0.2.0-rc.1", + "document-features", + "equihash", + "ff", + "fpe", + "getset", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "memuse", + "nonempty", + "orchard", + "rand 0.8.5", + "rand_core 0.6.4", + "redjubjub", + "ripemd 0.1.3", + "sapling-crypto", + "sha2 0.10.9", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_note_encryption", + "zcash_protocol", + "zcash_script", + "zcash_spec", + "zcash_transparent", + "zip32", +] + +[[package]] +name = "zcash_protocol" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b1a337bbc9a7d55ae35d31189f03507dbc7934e9a4bee5c1d5c47464860e48" +dependencies = [ + "core2", + "document-features", + "hex", + "memuse", +] + +[[package]] +name = "zcash_script" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ef9d04e0434a80b62ad06c5a610557be358ef60a98afa5dbc8ecaf19ad72e7" +dependencies = [ + "bip32", + "bitflags", + "bounded-vec", + "hex", + "ripemd 0.1.3", + "secp256k1", + "sha1", + "sha2 0.10.9", + "thiserror 2.0.18", +] + +[[package]] +name = "zcash_spec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zcash_transparent" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9b7b4bc11d8bb20833d1b8ab6807f4dca941b381f1129e5bbd72a84e391991" +dependencies = [ + "bip32", + "blake2b_simd", + "bs58", + "core2", + "document-features", + "getset", + "hex", + "nonempty", + "ripemd 0.1.3", + "secp256k1", + "sha2 0.10.9", + "subtle", + "zcash_address", + "zcash_encoding", + "zcash_protocol", + "zcash_script", + "zcash_spec", + "zip32", +] + [[package]] name = "zerocopy" version = "0.8.47" @@ -3101,6 +4199,19 @@ dependencies = [ "syn", ] +[[package]] +name = "zip32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64bf5186a8916f7a48f2a98ef599bf9c099e2458b36b819e393db1c0e768c4b" +dependencies = [ + "bech32 0.11.1", + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/ows/crates/ows-cli/Cargo.toml b/ows/crates/ows-cli/Cargo.toml index ec77bab4..055f80eb 100644 --- a/ows/crates/ows-cli/Cargo.toml +++ b/ows/crates/ows-cli/Cargo.toml @@ -11,6 +11,10 @@ readme = "README.md" name = "ows" path = "src/main.rs" +[features] +default = ["zcash-shielded"] +zcash-shielded = ["ows-lib/zcash-shielded", "ows-signer/zcash-shielded"] + [dependencies] ows-core = { path = "../ows-core", version = "=1.0.0" } ows-signer = { path = "../ows-signer", version = "=1.0.0" } diff --git a/ows/crates/ows-cli/src/commands/derive.rs b/ows/crates/ows-cli/src/commands/derive.rs index 01ada86a..7bc8b03f 100644 --- a/ows/crates/ows-cli/src/commands/derive.rs +++ b/ows/crates/ows-cli/src/commands/derive.rs @@ -4,33 +4,37 @@ use zeroize::Zeroize; use crate::{parse_chain, CliError}; +fn derive_address_for_chain( + mnemonic: &Mnemonic, + chain_type: ows_core::ChainType, + index: u32, +) -> Result { + let signer = signer_for_chain(chain_type); + + if signer.needs_raw_seed() { + let seed = mnemonic.to_seed(""); + Ok(signer.derive_address_from_seed(seed.expose(), index)?) + } else { + let path = signer.default_derivation_path(index); + let curve = signer.curve(); + let key = HdDeriver::derive_from_mnemonic_cached(mnemonic, "", &path, curve)?; + Ok(signer.derive_address(key.expose())?) + } +} + pub fn run(chain_str: Option<&str>, index: u32) -> Result<(), CliError> { let mut mnemonic_str = super::read_mnemonic()?; let mnemonic = Mnemonic::from_phrase(&mnemonic_str)?; mnemonic_str.zeroize(); if let Some(cs) = chain_str { - // Derive for a single chain let chain = parse_chain(cs)?; - let signer = signer_for_chain(chain.chain_type); - let path = signer.default_derivation_path(index); - let curve = signer.curve(); - - let key = HdDeriver::derive_from_mnemonic_cached(&mnemonic, "", &path, curve)?; - let address = signer.derive_address(key.expose())?; - + let address = derive_address_for_chain(&mnemonic, chain.chain_type, index)?; println!("{address}"); } else { - // Derive for all chains for ct in &ALL_CHAIN_TYPES { let chain = default_chain_for_type(*ct); - let signer = signer_for_chain(*ct); - let path = signer.default_derivation_path(index); - let curve = signer.curve(); - - let key = HdDeriver::derive_from_mnemonic_cached(&mnemonic, "", &path, curve)?; - let address = signer.derive_address(key.expose())?; - + let address = derive_address_for_chain(&mnemonic, *ct, index)?; println!("{} → {}", chain.chain_id, address); } } diff --git a/ows/crates/ows-cli/src/commands/send_transaction.rs b/ows/crates/ows-cli/src/commands/send_transaction.rs index 6dc4fe3a..7da58cef 100644 --- a/ows/crates/ows-cli/src/commands/send_transaction.rs +++ b/ows/crates/ows-cli/src/commands/send_transaction.rs @@ -38,8 +38,41 @@ pub fn run( return Ok(()); } - // Owner mode: resolve key directly (existing behavior) + // Owner mode let chain = parse_chain(chain_str)?; + + // Zcash PCZT: route through sign_and_send which handles seed-based key resolution. + // Must be checked before resolve_signing_key so passphrase handling is consistent. + #[cfg(feature = "zcash-shielded")] + if chain.chain_type == ows_core::ChainType::Zcash { + let result = match ows_lib::sign_and_send( + wallet_name, chain_str, tx_hex, Some(""), Some(index), rpc_url_override, None, + ) { + Ok(r) => r, + Err(ows_lib::OwsLibError::Crypto(_)) => { + let passphrase = super::read_passphrase(); + ows_lib::sign_and_send( + wallet_name, chain_str, tx_hex, + Some(&passphrase), Some(index), rpc_url_override, None, + )? + } + Err(e) => return Err(e.into()), + }; + + if json_output { + let obj = serde_json::json!({ + "tx_hash": result.tx_hash, + "chain": chain_str, + }); + println!("{}", serde_json::to_string_pretty(&obj)?); + } else { + println!("{}", result.tx_hash); + } + + audit::log_broadcast(wallet_name, chain_str, &result.tx_hash); + return Ok(()); + } + let key = super::resolve_signing_key(wallet_name, chain.chain_type, index)?; let tx_hex_clean = tx_hex.strip_prefix("0x").unwrap_or(tx_hex); diff --git a/ows/crates/ows-cli/src/commands/sign_message.rs b/ows/crates/ows-cli/src/commands/sign_message.rs index 92d180ad..fa8e12d6 100644 --- a/ows/crates/ows-cli/src/commands/sign_message.rs +++ b/ows/crates/ows-cli/src/commands/sign_message.rs @@ -35,8 +35,16 @@ pub fn run( return print_result(&result.signature, result.recovery_id, json_output); } - // Owner mode: resolve key directly (existing behavior) + // Owner mode let chain = parse_chain(chain_str)?; + + #[cfg(feature = "zcash-shielded")] + if chain.chain_type == ows_core::ChainType::Zcash { + return Err(CliError::InvalidArgs( + "message signing is not supported for Zcash shielded wallets".into(), + )); + } + let key = super::resolve_signing_key(wallet_name, chain.chain_type, index)?; let signer = signer_for_chain(chain.chain_type); diff --git a/ows/crates/ows-cli/src/commands/sign_transaction.rs b/ows/crates/ows-cli/src/commands/sign_transaction.rs index 7e491a30..fc99e07b 100644 --- a/ows/crates/ows-cli/src/commands/sign_transaction.rs +++ b/ows/crates/ows-cli/src/commands/sign_transaction.rs @@ -34,6 +34,14 @@ pub fn run( let tx_bytes = hex::decode(tx_hex_clean) .map_err(|e| CliError::InvalidArgs(format!("invalid hex transaction: {e}")))?; + // Zcash PCZT: the key is the raw seed; route through sign_pczt + #[cfg(feature = "zcash-shielded")] + if chain.chain_type == ows_core::ChainType::Zcash { + let zcash_signer = ows_signer::chains::ZcashSigner::from_chain_id(chain_str); + let signed_pczt = zcash_signer.sign_pczt(key.expose(), &tx_bytes, index)?; + return print_result(&hex::encode(&signed_pczt), None, json_output); + } + let signer = signer_for_chain(chain.chain_type); let output = signer.sign_transaction(key.expose(), &tx_bytes)?; diff --git a/ows/crates/ows-core/src/chain.rs b/ows/crates/ows-core/src/chain.rs index c857d860..87027491 100644 --- a/ows/crates/ows-core/src/chain.rs +++ b/ows/crates/ows-core/src/chain.rs @@ -14,10 +14,11 @@ pub enum ChainType { Spark, Filecoin, Sui, + Zcash, } /// All supported chain families, used for universal wallet derivation. -pub const ALL_CHAIN_TYPES: [ChainType; 8] = [ +pub const ALL_CHAIN_TYPES: [ChainType; 9] = [ ChainType::Evm, ChainType::Solana, ChainType::Bitcoin, @@ -26,6 +27,7 @@ pub const ALL_CHAIN_TYPES: [ChainType; 8] = [ ChainType::Ton, ChainType::Filecoin, ChainType::Sui, + ChainType::Zcash, ]; /// A specific chain (e.g. "ethereum", "arbitrum") with its family type and CAIP-2 ID. @@ -113,6 +115,11 @@ pub const KNOWN_CHAINS: &[Chain] = &[ chain_type: ChainType::Sui, chain_id: "sui:mainnet", }, + Chain { + name: "zcash", + chain_type: ChainType::Zcash, + chain_id: "zcash:mainnet", + }, ]; /// Parse a chain string into a `Chain`. Accepts: @@ -177,6 +184,7 @@ impl ChainType { ChainType::Spark => "spark", ChainType::Filecoin => "fil", ChainType::Sui => "sui", + ChainType::Zcash => "zcash", } } @@ -192,6 +200,7 @@ impl ChainType { ChainType::Spark => 8797555, ChainType::Filecoin => 461, ChainType::Sui => 784, + ChainType::Zcash => 133, } } @@ -207,6 +216,7 @@ impl ChainType { "spark" => Some(ChainType::Spark), "fil" => Some(ChainType::Filecoin), "sui" => Some(ChainType::Sui), + "zcash" => Some(ChainType::Zcash), _ => None, } } @@ -224,6 +234,7 @@ impl fmt::Display for ChainType { ChainType::Spark => "spark", ChainType::Filecoin => "filecoin", ChainType::Sui => "sui", + ChainType::Zcash => "zcash", }; write!(f, "{}", s) } @@ -243,6 +254,7 @@ impl FromStr for ChainType { "spark" => Ok(ChainType::Spark), "filecoin" => Ok(ChainType::Filecoin), "sui" => Ok(ChainType::Sui), + "zcash" => Ok(ChainType::Zcash), _ => Err(format!("unknown chain type: {}", s)), } } @@ -273,6 +285,7 @@ mod tests { (ChainType::Spark, "\"spark\""), (ChainType::Filecoin, "\"filecoin\""), (ChainType::Sui, "\"sui\""), + (ChainType::Zcash, "\"zcash\""), ] { let json = serde_json::to_string(&chain).unwrap(); assert_eq!(json, expected); @@ -292,6 +305,7 @@ mod tests { assert_eq!(ChainType::Spark.namespace(), "spark"); assert_eq!(ChainType::Filecoin.namespace(), "fil"); assert_eq!(ChainType::Sui.namespace(), "sui"); + assert_eq!(ChainType::Zcash.namespace(), "zcash"); } #[test] @@ -305,6 +319,7 @@ mod tests { assert_eq!(ChainType::Spark.default_coin_type(), 8797555); assert_eq!(ChainType::Filecoin.default_coin_type(), 461); assert_eq!(ChainType::Sui.default_coin_type(), 784); + assert_eq!(ChainType::Zcash.default_coin_type(), 133); } #[test] @@ -321,6 +336,7 @@ mod tests { assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark)); assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin)); assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui)); + assert_eq!(ChainType::from_namespace("zcash"), Some(ChainType::Zcash)); assert_eq!(ChainType::from_namespace("unknown"), None); } @@ -372,7 +388,7 @@ mod tests { #[test] fn test_all_chain_types() { - assert_eq!(ALL_CHAIN_TYPES.len(), 8); + assert_eq!(ALL_CHAIN_TYPES.len(), 9); } #[test] diff --git a/ows/crates/ows-core/src/config.rs b/ows/crates/ows-core/src/config.rs index 3d45a2d1..c19e7500 100644 --- a/ows/crates/ows-core/src/config.rs +++ b/ows/crates/ows-core/src/config.rs @@ -63,6 +63,14 @@ impl Config { "sui:mainnet".into(), "https://fullnode.mainnet.sui.io:443".into(), ); + rpc.insert( + "zcash:mainnet".into(), + "https://zec.rocks:443".into(), + ); + rpc.insert( + "zcash:testnet".into(), + "https://testnet.zec.rocks:443".into(), + ); rpc } } @@ -242,7 +250,7 @@ mod tests { fn test_load_or_default_nonexistent() { let config = Config::load_or_default_from(std::path::Path::new("/nonexistent/config.json")); // Should have all default RPCs - assert_eq!(config.rpc.len(), 14); + assert_eq!(config.rpc.len(), 16); assert_eq!(config.rpc_url("eip155:1"), Some("https://eth.llamarpc.com")); } diff --git a/ows/crates/ows-lib/Cargo.toml b/ows/crates/ows-lib/Cargo.toml index 3a46c17a..a8dd95d3 100644 --- a/ows/crates/ows-lib/Cargo.toml +++ b/ows/crates/ows-lib/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" [features] default = [] fast-kdf = ["ows-signer/fast-kdf"] +zcash-shielded = ["ows-signer/zcash-shielded", "pczt", "zcash_primitives", "tonic/tls", "tonic/tls-webpki-roots"] [dependencies] ows-core = { path = "../ows-core", version = "=1.0.0" } @@ -28,6 +29,8 @@ rand = "0.8" prost = "0.13" tonic = { version = "0.12", features = ["transport"] } tokio = { version = "1", features = ["rt"] } +pczt = { version = "0.5", features = ["tx-extractor"], optional = true } +zcash_primitives = { version = "0.26", optional = true } [dev-dependencies] tempfile = "3" diff --git a/ows/crates/ows-lib/src/lib.rs b/ows/crates/ows-lib/src/lib.rs index cfa6e859..13f0bc0d 100644 --- a/ows/crates/ows-lib/src/lib.rs +++ b/ows/crates/ows-lib/src/lib.rs @@ -1,6 +1,8 @@ pub mod error; pub mod key_ops; pub mod key_store; +#[cfg(feature = "zcash-shielded")] +pub mod lwd_grpc; pub mod migrate; pub mod ops; pub mod policy_engine; diff --git a/ows/crates/ows-lib/src/lwd_grpc.rs b/ows/crates/ows-lib/src/lwd_grpc.rs new file mode 100644 index 00000000..e6df45e6 --- /dev/null +++ b/ows/crates/ows-lib/src/lwd_grpc.rs @@ -0,0 +1,84 @@ +use crate::error::OwsLibError; + +/// Hand-written prost message types matching the lightwalletd gRPC proto. +/// See: https://github.com/zcash/lightwalletd/blob/master/walletrpc/service.proto + +#[derive(Clone, PartialEq, prost::Message)] +pub struct RawTransaction { + #[prost(bytes = "vec", tag = "1")] + pub data: Vec, + #[prost(uint64, tag = "2")] + pub height: u64, +} + +#[derive(Clone, PartialEq, prost::Message)] +pub struct SendResponse { + #[prost(int32, tag = "1")] + pub error_code: i32, + #[prost(string, tag = "2")] + pub error_message: String, +} + +/// Send a raw Zcash transaction to a lightwalletd instance via gRPC. +/// +/// `endpoint` is the lightwalletd gRPC URL (e.g. `https://zec.rocks:443`). +/// `tx_bytes` is the fully serialized, finalized Zcash transaction. +/// +/// Returns the transaction ID (txid) as a hex string on success. +pub fn send_transaction(endpoint: &str, tx_bytes: &[u8]) -> Result { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| OwsLibError::BroadcastFailed(format!("failed to create runtime: {e}")))?; + + rt.block_on(async { + let tls = tonic::transport::ClientTlsConfig::new() + .with_webpki_roots(); + let channel = tonic::transport::Channel::from_shared(endpoint.to_string()) + .map_err(|e| OwsLibError::BroadcastFailed(format!("invalid endpoint: {e}")))? + .tls_config(tls) + .map_err(|e| OwsLibError::BroadcastFailed(format!("TLS config failed: {e}")))? + .connect() + .await + .map_err(|e| OwsLibError::BroadcastFailed(format!("gRPC connect failed: {e}")))?; + + let mut client = tonic::client::Grpc::new(channel); + + client + .ready() + .await + .map_err(|e| OwsLibError::BroadcastFailed(format!("gRPC not ready: {e}")))?; + + let request = RawTransaction { + data: tx_bytes.to_vec(), + height: 0, + }; + + let path = tonic::codegen::http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction", + ); + let codec = tonic::codec::ProstCodec::default(); + + let response: tonic::Response = client + .unary(tonic::Request::new(request), path, codec) + .await + .map_err(|e| OwsLibError::BroadcastFailed(format!("gRPC SendTransaction error: {e}")))?; + + let resp = response.into_inner(); + if resp.error_code != 0 { + return Err(OwsLibError::BroadcastFailed(format!( + "lightwalletd rejected tx (code {}): {}", + resp.error_code, resp.error_message + ))); + } + + // lightwalletd returns the txid in the error_message field on success + if resp.error_message.is_empty() { + Err(OwsLibError::BroadcastFailed( + "broadcast succeeded but lightwalletd did not return a txid".into(), + )) + } else { + Ok(resp.error_message) + } + }) +} diff --git a/ows/crates/ows-lib/src/ops.rs b/ows/crates/ows-lib/src/ops.rs index 6eca56b0..c38ae047 100644 --- a/ows/crates/ows-lib/src/ops.rs +++ b/ows/crates/ows-lib/src/ops.rs @@ -42,10 +42,19 @@ fn derive_all_accounts(mnemonic: &Mnemonic, index: u32) -> Result, + rpc_url: Option<&str>, + chain_str: &str, +) -> Result { + let zcash_signer = ows_signer::chains::ZcashSigner::from_chain_id(chain_str); + let signed_pczt_bytes = zcash_signer.sign_pczt(seed, pczt_bytes, index.unwrap_or(0))?; + + let signed_pczt = pczt::Pczt::parse(&signed_pczt_bytes).map_err(|e| { + OwsLibError::InvalidInput(format!("failed to parse signed PCZT: {e:?}")) + })?; + + let tx = pczt::roles::tx_extractor::TransactionExtractor::new(signed_pczt) + .extract() + .map_err(|e| { + OwsLibError::InvalidInput(format!("PCZT finalization failed: {e:?}")) + })?; + + let mut tx_bytes = Vec::new(); + tx.write(&mut tx_bytes).map_err(|e| { + OwsLibError::InvalidInput(format!("failed to serialize Zcash transaction: {e}")) + })?; + + let chain = parse_chain(chain_str)?; + let rpc = resolve_rpc_url(chain.chain_id, chain.chain_type, rpc_url)?; + let tx_hash = broadcast(chain.chain_type, &rpc, &tx_bytes)?; + + Ok(SendResult { tx_hash }) +} + /// Sign, encode, and broadcast a transaction using an already-resolved private key. /// /// This is the shared core of the send-transaction flow. Both the library's @@ -612,17 +721,22 @@ pub fn decrypt_signing_key( match wallet.key_type { KeyType::Mnemonic => { - // Use the SecretBytes directly as a &str to avoid un-zeroized String copies. let phrase = std::str::from_utf8(secret.expose()).map_err(|_| { OwsLibError::InvalidInput("wallet contains invalid UTF-8 mnemonic".into()) })?; let mnemonic = Mnemonic::from_phrase(phrase)?; let signer = signer_for_chain(chain_type); - let path = signer.default_derivation_path(index.unwrap_or(0)); - let curve = signer.curve(); - Ok(HdDeriver::derive_from_mnemonic_cached( - &mnemonic, "", &path, curve, - )?) + + if signer.needs_raw_seed() { + let seed = mnemonic.to_seed(""); + Ok(seed) + } else { + let path = signer.default_derivation_path(index.unwrap_or(0)); + let curve = signer.curve(); + Ok(HdDeriver::derive_from_mnemonic_cached( + &mnemonic, "", &path, curve, + )?) + } } KeyType::PrivateKey => { // JSON key pair — extract the right key for this chain's curve @@ -688,6 +802,12 @@ fn broadcast(chain: ChainType, rpc_url: &str, signed_bytes: &[u8]) -> Result broadcast_sui(rpc_url, signed_bytes), + #[cfg(feature = "zcash-shielded")] + ChainType::Zcash => broadcast_zcash(rpc_url, signed_bytes), + #[cfg(not(feature = "zcash-shielded"))] + ChainType::Zcash => Err(OwsLibError::InvalidInput( + "Zcash broadcast requires the zcash-shielded feature".into(), + )), } } @@ -802,6 +922,11 @@ fn broadcast_sui(rpc_url: &str, signed_bytes: &[u8]) -> Result Result { + crate::lwd_grpc::send_transaction(rpc_url, tx_bytes) +} + fn curl_post_json(url: &str, body: &str) -> Result { let output = Command::new("curl") .args([ @@ -2607,4 +2732,236 @@ mod tests { "sign_message owner path must match direct signer" ); } + + // ================================================================ + // ZCASH INTEGRATION TESTS + // ================================================================ + + #[test] + fn zcash_wallet_create_includes_zcash_account() { + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + let info = create_wallet("zec-create", None, None, Some(vault)).unwrap(); + + let zcash_account = info + .accounts + .iter() + .find(|a| a.chain_id == "zcash:mainnet"); + assert!( + zcash_account.is_some(), + "wallet must include a zcash:mainnet account" + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_wallet_create_derives_unified_address() { + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + let info = create_wallet("zec-ua", None, None, Some(vault)).unwrap(); + + let zcash_account = info + .accounts + .iter() + .find(|a| a.chain_id == "zcash:mainnet") + .expect("wallet must include a zcash:mainnet account"); + + assert!( + zcash_account.address.starts_with("u1"), + "Zcash address should be a unified address (u1...), got: {}", + zcash_account.address + ); + assert!( + zcash_account.address.len() > 50, + "unified address should be long (multiple receivers), got len: {}", + zcash_account.address.len() + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_derive_address_produces_unified_address() { + let phrase = generate_mnemonic(12).unwrap(); + let addr = derive_address(&phrase, "zcash", None).unwrap(); + assert!( + addr.starts_with("u1"), + "derived Zcash address should be unified (u1...), got: {}", + addr + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_derive_address_deterministic() { + let phrase = generate_mnemonic(12).unwrap(); + let a = derive_address(&phrase, "zcash", None).unwrap(); + let b = derive_address(&phrase, "zcash", None).unwrap(); + assert_eq!(a, b, "same mnemonic must produce same Zcash unified address"); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_derive_address_different_accounts() { + let phrase = generate_mnemonic(12).unwrap(); + let a = derive_address(&phrase, "zcash", Some(0)).unwrap(); + let b = derive_address(&phrase, "zcash", Some(1)).unwrap(); + assert_ne!(a, b, "different account indices must produce different addresses"); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_import_wallet_address_matches_derivation() { + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + let info = + import_wallet_mnemonic("zec-import", phrase, None, None, Some(vault)).unwrap(); + let zcash_account = info + .accounts + .iter() + .find(|a| a.chain_id == "zcash:mainnet") + .expect("imported wallet must include Zcash account"); + + let derived = derive_address(phrase, "zcash", None).unwrap(); + assert_eq!( + zcash_account.address, derived, + "wallet import and direct derivation must produce the same Zcash address" + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_wallet_export_reimport_address_stable() { + let v1 = tempfile::tempdir().unwrap(); + let v2 = tempfile::tempdir().unwrap(); + + let w1 = create_wallet("zec-export", None, None, Some(v1.path())).unwrap(); + let phrase = export_wallet("zec-export", None, Some(v1.path())).unwrap(); + + let w2 = + import_wallet_mnemonic("zec-reimport", &phrase, None, None, Some(v2.path())).unwrap(); + + let addr1 = w1 + .accounts + .iter() + .find(|a| a.chain_id == "zcash:mainnet") + .unwrap() + .address + .clone(); + let addr2 = w2 + .accounts + .iter() + .find(|a| a.chain_id == "zcash:mainnet") + .unwrap() + .address + .clone(); + + assert_eq!( + addr1, addr2, + "Zcash address must be stable across export → reimport" + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_decrypt_signing_key_returns_seed() { + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + create_wallet("zec-key", None, None, Some(vault)).unwrap(); + + let key = decrypt_signing_key("zec-key", ChainType::Zcash, "", None, Some(vault)).unwrap(); + assert_eq!( + key.expose().len(), + 64, + "Zcash signing key should be the raw 64-byte BIP-39 seed" + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_sign_transaction_accepts_pczt_bytes() { + use pczt::roles::creator::Creator; + + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + create_wallet("zec-sign", None, None, Some(vault)).unwrap(); + + // Build a minimal empty PCZT via the Creator role. + // No inputs = no signatures needed, but this verifies the full + // PCZT routing through the library (hex decode → seed decrypt → + // ZcashSigner::sign_pczt → return signed bytes). + let pczt = Creator::new( + 0xc2d6_d0b4, // NU5 consensus branch ID + 0, // expiry height + 133, // coin type (Zcash) + [0u8; 32], // sapling anchor + [0u8; 32], // orchard anchor + ) + .build(); + let pczt_hex = hex::encode(pczt.serialize()); + + let result = sign_transaction("zec-sign", "zcash", &pczt_hex, None, None, Some(vault)); + assert!( + result.is_ok(), + "signing an empty PCZT should succeed (no inputs to sign): {:?}", + result.err() + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_sign_transaction_rejects_garbage() { + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + create_wallet("zec-garbage", None, None, Some(vault)).unwrap(); + + let garbage_hex = "deadbeefcafebabe"; + let result = + sign_transaction("zec-garbage", "zcash", garbage_hex, None, None, Some(vault)); + assert!( + result.is_err(), + "signing garbage bytes should fail with a PCZT parse error" + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_24_word_mnemonic_derives_valid_address() { + let phrase = generate_mnemonic(24).unwrap(); + let addr = derive_address(&phrase, "zcash", None).unwrap(); + assert!( + addr.starts_with("u1"), + "24-word mnemonic should produce unified address, got: {}", + addr + ); + } + + #[cfg(feature = "zcash-shielded")] + #[test] + fn zcash_wallet_create_other_chains_unaffected() { + let dir = tempfile::tempdir().unwrap(); + let vault = dir.path(); + let info = create_wallet("zec-others", None, None, Some(vault)).unwrap(); + + let evm = info.accounts.iter().find(|a| a.chain_id.starts_with("eip155:")); + let sol = info.accounts.iter().find(|a| a.chain_id.starts_with("solana:")); + let btc = info.accounts.iter().find(|a| a.chain_id.starts_with("bip122:")); + let zec = info.accounts.iter().find(|a| a.chain_id == "zcash:mainnet"); + + assert!(evm.is_some(), "EVM account must exist"); + assert!(sol.is_some(), "Solana account must exist"); + assert!(btc.is_some(), "Bitcoin account must exist"); + assert!(zec.is_some(), "Zcash account must exist"); + + assert!(evm.unwrap().address.starts_with("0x"), "EVM format preserved"); + assert!(zec.unwrap().address.starts_with("u1"), "Zcash format correct"); + + let evm_sig = sign_message("zec-others", "evm", "test", None, None, None, Some(vault)); + assert!( + evm_sig.is_ok(), + "EVM signing must still work after Zcash integration: {:?}", + evm_sig.err() + ); + } } diff --git a/ows/crates/ows-signer/Cargo.toml b/ows/crates/ows-signer/Cargo.toml index 95a96c64..2051e00d 100644 --- a/ows/crates/ows-signer/Cargo.toml +++ b/ows/crates/ows-signer/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" [features] default = [] fast-kdf = [] +zcash-shielded = ["zcash_keys", "zcash_protocol", "zip32", "pczt", "sapling-crypto", "orchard", "zcash_transparent"] [dependencies] ows-core = { path = "../ows-core", version = "=1.0.0" } @@ -38,4 +39,13 @@ digest = "0.10" libc = "0.2" signal-hook = "0.4" +# Zcash shielded support (unified addresses, PCZT signing) — heavy deps, feature-gated +zcash_keys = { version = "0.12", features = ["orchard", "sapling", "transparent-inputs"], optional = true } +zcash_protocol = { version = "0.7", optional = true } +zip32 = { version = "0.2", optional = true } +pczt = { version = "0.5", features = ["signer"], optional = true } +sapling-crypto = { version = "0.5", optional = true } +orchard = { version = "0.11", optional = true } +zcash_transparent = { version = "0.6", optional = true } + [dev-dependencies] diff --git a/ows/crates/ows-signer/src/chains/mod.rs b/ows/crates/ows-signer/src/chains/mod.rs index b73d8a24..95162216 100644 --- a/ows/crates/ows-signer/src/chains/mod.rs +++ b/ows/crates/ows-signer/src/chains/mod.rs @@ -7,6 +7,7 @@ pub mod spark; pub mod sui; pub mod ton; pub mod tron; +pub mod zcash; pub use self::bitcoin::BitcoinSigner; pub use self::cosmos::CosmosSigner; @@ -17,6 +18,7 @@ pub use self::spark::SparkSigner; pub use self::sui::SuiSigner; pub use self::ton::TonSigner; pub use self::tron::TronSigner; +pub use self::zcash::ZcashSigner; use crate::traits::ChainSigner; use ows_core::ChainType; @@ -33,5 +35,6 @@ pub fn signer_for_chain(chain: ChainType) -> Box { ChainType::Spark => Box::new(SparkSigner), ChainType::Filecoin => Box::new(FilecoinSigner), ChainType::Sui => Box::new(SuiSigner), + ChainType::Zcash => Box::new(ZcashSigner::mainnet()), } } diff --git a/ows/crates/ows-signer/src/chains/zcash.rs b/ows/crates/ows-signer/src/chains/zcash.rs new file mode 100644 index 00000000..aa844d5a --- /dev/null +++ b/ows/crates/ows-signer/src/chains/zcash.rs @@ -0,0 +1,648 @@ +use crate::curve::Curve; +use crate::traits::{ChainSigner, SignOutput, SignerError}; +use k256::ecdsa::SigningKey; +use ows_core::ChainType; +use ripemd::Ripemd160; +use sha2::{Digest, Sha256}; + +#[cfg(feature = "zcash-shielded")] +use zcash_keys::keys::{ReceiverRequirement, UnifiedSpendingKey}; +#[cfg(feature = "zcash-shielded")] +use zcash_protocol::consensus::Network; + +/// Zcash transparent chain signer. +/// +/// Handles t-address derivation and transparent transaction signing. +/// Zcash transparent uses the same secp256k1 curve as Bitcoin with +/// a different address encoding (Base58Check with Zcash-specific +/// two-byte version prefix, per § 5.6.1 of the protocol spec). +/// +/// # Transaction signing +/// +/// Zcash sighash computation uses BLAKE2b-256 with a consensus-branch-specific +/// personalization string (ZIP-244). Because this requires knowledge of the +/// consensus branch ID, `sign_transaction` expects a pre-computed 32-byte +/// sighash — not raw transaction bytes. The caller (e.g. a PCZT builder or +/// lightwalletd client) is responsible for computing the sighash per ZIP-244. +/// +/// # Message signing +/// +/// Follows the same format as zcashd's `signmessage` RPC: a double-SHA256 +/// of the magic-prefixed message, using "Zcash Signed Message:\n" as the +/// magic string (cf. `strMessageMagic` in zcash/src/main.cpp). +/// +/// # Shielded transactions +/// +/// Shielded (Sapling/Orchard) transactions require zero-knowledge proof +/// generation and use the PCZT format — see the companion RFC. +pub struct ZcashSigner { + /// Two-byte Base58Check version prefix. + /// Mainnet P2PKH: [0x1C, 0xB8] → t1... + /// Testnet P2PKH: [0x1D, 0x25] → tm... + /// (cf. chainparams.cpp base58Prefixes[PUBKEY_ADDRESS]) + addr_version: [u8; 2], +} + +impl ZcashSigner { + pub fn new(addr_version: [u8; 2]) -> Self { + ZcashSigner { addr_version } + } + + pub fn mainnet() -> Self { + Self::new([0x1C, 0xB8]) + } + + pub fn testnet() -> Self { + Self::new([0x1D, 0x25]) + } + + /// Select mainnet or testnet based on CAIP-2 chain identifier. + pub fn from_chain_id(chain_id: &str) -> Self { + if chain_id.contains("testnet") { + Self::testnet() + } else { + Self::mainnet() + } + } + + #[cfg(feature = "zcash-shielded")] + fn network(&self) -> Network { + if self.addr_version == [0x1C, 0xB8] { + Network::MainNetwork + } else { + Network::TestNetwork + } + } + + /// Derive a unified address (t + sapling + orchard receivers) from a + /// raw BIP-39 seed using ZIP-32 derivation. + #[cfg(feature = "zcash-shielded")] + pub fn derive_unified_address( + &self, + seed: &[u8], + account_index: u32, + ) -> Result { + if seed.len() < 32 { + return Err(SignerError::AddressDerivationFailed(format!( + "seed must be at least 32 bytes, got {}", + seed.len() + ))); + } + + let network = self.network(); + let account = zip32::AccountId::try_from(account_index).map_err(|e| { + SignerError::AddressDerivationFailed(format!("invalid account index: {e}")) + })?; + + let usk = UnifiedSpendingKey::from_seed(&network, seed, account).map_err(|e| { + SignerError::AddressDerivationFailed(format!("ZIP-32 key derivation failed: {e:?}")) + })?; + + let ufvk = usk.to_unified_full_viewing_key(); + + let request = zcash_keys::keys::UnifiedAddressRequest::unsafe_custom( + ReceiverRequirement::Require, + ReceiverRequirement::Require, + ReceiverRequirement::Omit, + ); + + let (ua, _diversifier_index) = ufvk.default_address(request).map_err(|e| { + SignerError::AddressDerivationFailed(format!( + "unified address derivation failed: {e}" + )) + })?; + + Ok(ua.encode(&network)) + } + + /// Sign a PCZT (Partially Created Zcash Transaction). + /// + /// OWS acts as the Signer role: it receives a PCZT that has already been + /// through Creator + Prover, applies spend authorization signatures for + /// all transparent, Sapling, and Orchard inputs, and returns the signed PCZT. + /// + /// The `seed` must be the raw BIP-39 seed (64 bytes). The USK is derived + /// via ZIP-32 to extract the per-pool signing keys. + #[cfg(feature = "zcash-shielded")] + pub fn sign_pczt( + &self, + seed: &[u8], + pczt_bytes: &[u8], + account_index: u32, + ) -> Result, SignerError> { + use zcash_transparent::keys::NonHardenedChildIndex; + + if seed.len() < 32 { + return Err(SignerError::InvalidPrivateKey(format!( + "seed must be at least 32 bytes, got {}", + seed.len() + ))); + } + + let network = self.network(); + let account = zip32::AccountId::try_from(account_index).map_err(|e| { + SignerError::SigningFailed(format!("invalid account index: {e}")) + })?; + + let usk = UnifiedSpendingKey::from_seed(&network, seed, account).map_err(|e| { + SignerError::SigningFailed(format!("ZIP-32 key derivation failed: {e:?}")) + })?; + + let pczt = pczt::Pczt::parse(pczt_bytes).map_err(|e| { + SignerError::InvalidTransaction(format!("failed to parse PCZT: {e:?}")) + })?; + + let n_transparent = pczt.transparent().inputs().len(); + let n_sapling = pczt.sapling().spends().len(); + let n_orchard = pczt.orchard().actions().len(); + + let mut signer = pczt::roles::signer::Signer::new(pczt).map_err(|e| { + SignerError::SigningFailed(format!("failed to initialize PCZT signer: {e:?}")) + })?; + + if n_transparent > 0 { + // Derive external scope at index 0 (the primary receiving address). + // Inputs at other indices are skipped via TransparentSign error below, + // consistent with how Sapling/Orchard skip non-matching keys. + let scope = zcash_transparent::keys::TransparentKeyScope::from( + zip32::Scope::External, + ); + let transparent_sk = usk + .transparent() + .derive_secret_key(scope, NonHardenedChildIndex::ZERO) + .map_err(|e| { + SignerError::SigningFailed(format!( + "failed to derive transparent secret key: {e:?}" + )) + })?; + for i in 0..n_transparent { + match signer.sign_transparent(i, &transparent_sk) { + Ok(()) => {} + Err(pczt::roles::signer::Error::TransparentSign(_)) => { + // Not our input (different key) — skip it + } + Err(e) => { + return Err(SignerError::SigningFailed(format!( + "transparent input {i} signing failed: {e:?}" + ))); + } + } + } + } + + if n_sapling > 0 { + let sapling_ask = &usk.sapling().expsk.ask; + for i in 0..n_sapling { + match signer.sign_sapling(i, sapling_ask) { + Ok(()) => {} + Err(pczt::roles::signer::Error::SaplingSign(_)) => { + // Not our spend (different key or dummy) — skip it + } + Err(e) => { + return Err(SignerError::SigningFailed(format!( + "sapling spend {i} signing failed: {e:?}" + ))); + } + } + } + } + + if n_orchard > 0 { + let orchard_ask = orchard::keys::SpendAuthorizingKey::from(usk.orchard()); + for i in 0..n_orchard { + match signer.sign_orchard(i, &orchard_ask) { + Ok(()) => {} + Err(pczt::roles::signer::Error::OrchardSign( + orchard::pczt::SignerError::WrongSpendAuthorizingKey, + )) => { + // Not our action (dummy/padding spend) — skip it + } + Err(e) => { + return Err(SignerError::SigningFailed(format!( + "orchard action {i} signing failed: {e:?}" + ))); + } + } + } + } + + let signed_pczt = signer.finish(); + Ok(signed_pczt.serialize()) + } + + fn signing_key(private_key: &[u8]) -> Result { + SigningKey::from_slice(private_key) + .map_err(|e| SignerError::InvalidPrivateKey(e.to_string())) + } + + fn hash160(data: &[u8]) -> Vec { + let sha256 = Sha256::digest(data); + let ripemd = Ripemd160::digest(sha256); + ripemd.to_vec() + } +} + +/// Encode an integer as a Bitcoin/Zcash CompactSize (varint). +fn encode_compact_size(buf: &mut Vec, n: usize) { + if n < 253 { + buf.push(n as u8); + } else if n <= 0xFFFF { + buf.push(0xFD); + buf.extend_from_slice(&(n as u16).to_le_bytes()); + } else if n <= 0xFFFF_FFFF { + buf.push(0xFE); + buf.extend_from_slice(&(n as u32).to_le_bytes()); + } else { + buf.push(0xFF); + buf.extend_from_slice(&(n as u64).to_le_bytes()); + } +} + +impl ChainSigner for ZcashSigner { + fn chain_type(&self) -> ChainType { + ChainType::Zcash + } + + fn curve(&self) -> Curve { + Curve::Secp256k1 + } + + fn coin_type(&self) -> u32 { + 133 + } + + fn derive_address(&self, private_key: &[u8]) -> Result { + let signing_key = Self::signing_key(private_key)?; + let verifying_key = signing_key.verifying_key(); + + let pubkey_compressed = verifying_key.to_encoded_point(true); + let pubkey_bytes = pubkey_compressed.as_bytes(); + + let hash = Self::hash160(pubkey_bytes); + + // Base58Check: version_bytes (2) ++ hash160 (20) ++ checksum (4) + let mut payload = Vec::with_capacity(2 + 20 + 4); + payload.extend_from_slice(&self.addr_version); + payload.extend_from_slice(&hash); + + let checksum = Sha256::digest(Sha256::digest(&payload)); + payload.extend_from_slice(&checksum[..4]); + + Ok(bs58::encode(&payload).into_string()) + } + + fn sign(&self, private_key: &[u8], message: &[u8]) -> Result { + if message.len() != 32 { + return Err(SignerError::InvalidMessage(format!( + "expected 32-byte hash, got {} bytes", + message.len() + ))); + } + + let signing_key = Self::signing_key(private_key)?; + let (signature, recovery_id) = signing_key + .sign_prehash_recoverable(message) + .map_err(|e| SignerError::SigningFailed(e.to_string()))?; + + let mut sig_bytes = signature.to_bytes().to_vec(); + sig_bytes.push(recovery_id.to_byte()); + + Ok(SignOutput { + signature: sig_bytes, + recovery_id: Some(recovery_id.to_byte()), + public_key: None, + }) + } + + fn sign_transaction( + &self, + private_key: &[u8], + tx_bytes: &[u8], + ) -> Result { + // Zcash transaction sighash is computed per ZIP-244 using BLAKE2b-256 + // with a consensus-branch-specific personalization ("ZcashTxHash_" || + // CONSENSUS_BRANCH_ID). Because the signer does not know the branch ID, + // callers must pre-compute the 32-byte sighash and pass it directly. + // + // This matches how PCZT (Partially Created Zcash Transaction) works: + // the Creator/Prover computes the sighash, and the Signer just signs it. + if tx_bytes.len() != 32 { + return Err(SignerError::InvalidTransaction( + "Zcash requires a pre-computed 32-byte sighash per ZIP-244. \ + Raw transaction bytes are not supported — use a transaction \ + builder to compute the sighash (BLAKE2b-256 with branch-specific \ + personalization) before calling sign_transaction." + .to_string(), + )); + } + self.sign(private_key, tx_bytes) + } + + fn sign_message(&self, private_key: &[u8], message: &[u8]) -> Result { + // Zcash message signing follows the same format as zcashd signmessage RPC: + // double-SHA256 of (magic_prefix || CompactSize(message_len) || message). + // + // Magic: "\x16Zcash Signed Message:\n" + // \x16 = 22 = byte length of "Zcash Signed Message:\n" + // (cf. strMessageMagic in zcash/src/main.cpp) + let prefix = b"\x16Zcash Signed Message:\n"; + let mut data = Vec::new(); + data.extend_from_slice(prefix); + encode_compact_size(&mut data, message.len()); + data.extend_from_slice(message); + + let hash = Sha256::digest(Sha256::digest(&data)); + self.sign(private_key, &hash) + } + + fn default_derivation_path(&self, index: u32) -> String { + format!("m/44'/133'/0'/0/{}", index) + } + + #[cfg(feature = "zcash-shielded")] + fn needs_raw_seed(&self) -> bool { + true + } + + #[cfg(feature = "zcash-shielded")] + fn derive_address_from_seed( + &self, + seed: &[u8], + account_index: u32, + ) -> Result { + self.derive_unified_address(seed, account_index) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_privkey() -> Vec { + // Private key = 1 (generator point G) + let mut privkey = vec![0u8; 31]; + privkey.push(1u8); + privkey + } + + #[test] + fn test_mainnet_t_address_known_value() { + // Private key = 1 → secp256k1 generator point G + // Compressed pubkey: 0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 + // hash160: 751e76e8199196d454941c45d1b3a323f1433bd6 + // Base58Check with version [0x1C, 0xB8] → t1UYsZVJkLPeMjxEtACvSxfWuNmddpWfxzs + // (independently verified via Python and cross-referenced with Bitcoin's + // 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH which shares the same hash160) + let signer = ZcashSigner::mainnet(); + let address = signer.derive_address(&test_privkey()).unwrap(); + assert_eq!( + address, "t1UYsZVJkLPeMjxEtACvSxfWuNmddpWfxzs", + "address must match known value for generator point" + ); + } + + #[test] + fn test_testnet_tm_address() { + let signer = ZcashSigner::testnet(); + let address = signer.derive_address(&test_privkey()).unwrap(); + assert!( + address.starts_with("tm"), + "testnet address should start with tm, got: {}", + address + ); + assert_eq!(address.len(), 35, "t-addr must be 35 characters"); + } + + #[test] + fn test_chain_properties() { + let signer = ZcashSigner::mainnet(); + assert_eq!(signer.chain_type(), ChainType::Zcash); + assert_eq!(signer.curve(), Curve::Secp256k1); + assert_eq!(signer.coin_type(), 133); + } + + #[test] + fn test_derivation_path() { + let signer = ZcashSigner::mainnet(); + assert_eq!(signer.default_derivation_path(0), "m/44'/133'/0'/0/0"); + assert_eq!(signer.default_derivation_path(3), "m/44'/133'/0'/0/3"); + } + + #[test] + fn test_sign_transaction_accepts_32_byte_sighash() { + use k256::ecdsa::signature::hazmat::PrehashVerifier; + + let signer = ZcashSigner::mainnet(); + let privkey = test_privkey(); + + // Simulate a pre-computed 32-byte sighash (as ZIP-244 would produce) + let sighash = Sha256::digest(b"simulated ZIP-244 sighash preimage"); + + let result = signer.sign_transaction(&privkey, &sighash).unwrap(); + + let signing_key = SigningKey::from_slice(&privkey).unwrap(); + let verifying_key = signing_key.verifying_key(); + let r: [u8; 32] = result.signature[..32].try_into().unwrap(); + let s: [u8; 32] = result.signature[32..64].try_into().unwrap(); + let sig = k256::ecdsa::Signature::from_scalars(r, s).unwrap(); + + verifying_key + .verify_prehash(&sighash, &sig) + .expect("signature must verify against the provided sighash"); + } + + #[test] + fn test_sign_transaction_rejects_raw_bytes() { + let signer = ZcashSigner::mainnet(); + let privkey = test_privkey(); + + let result = signer.sign_transaction(&privkey, b"not a 32-byte sighash"); + assert!(result.is_err(), "must reject non-32-byte input"); + + let err = result.unwrap_err().to_string(); + assert!( + err.contains("ZIP-244"), + "error should reference ZIP-244, got: {}", + err + ); + } + + #[test] + fn test_sign_message_short() { + use k256::ecdsa::signature::hazmat::PrehashVerifier; + + let signer = ZcashSigner::mainnet(); + let privkey = test_privkey(); + let message = b"Hello Zcash!"; + + let result = signer.sign_message(&privkey, message).unwrap(); + + // Verify against correctly constructed hash: + // \x16 (22) + "Zcash Signed Message:\n" + CompactSize(12) + message + let mut expected_data = Vec::new(); + expected_data.extend_from_slice(b"\x16Zcash Signed Message:\n"); + expected_data.push(message.len() as u8); + expected_data.extend_from_slice(message); + let expected_hash = Sha256::digest(Sha256::digest(&expected_data)); + + let signing_key = SigningKey::from_slice(&privkey).unwrap(); + let verifying_key = signing_key.verifying_key(); + let r: [u8; 32] = result.signature[..32].try_into().unwrap(); + let s: [u8; 32] = result.signature[32..64].try_into().unwrap(); + let sig = k256::ecdsa::Signature::from_scalars(r, s).unwrap(); + + verifying_key + .verify_prehash(&expected_hash, &sig) + .expect("signature must verify for short messages"); + } + + #[test] + fn test_sign_message_long_varint() { + use k256::ecdsa::signature::hazmat::PrehashVerifier; + + let signer = ZcashSigner::mainnet(); + let privkey = test_privkey(); + let message = vec![0x42u8; 300]; + + let result = signer.sign_message(&privkey, &message).unwrap(); + + // CompactSize for 300: 0xFD followed by 300 as 2-byte LE + let mut expected_data = Vec::new(); + expected_data.extend_from_slice(b"\x16Zcash Signed Message:\n"); + expected_data.push(0xFD); + expected_data.extend_from_slice(&300u16.to_le_bytes()); + expected_data.extend_from_slice(&message); + let expected_hash = Sha256::digest(Sha256::digest(&expected_data)); + + let signing_key = SigningKey::from_slice(&privkey).unwrap(); + let verifying_key = signing_key.verifying_key(); + let r: [u8; 32] = result.signature[..32].try_into().unwrap(); + let s: [u8; 32] = result.signature[32..64].try_into().unwrap(); + let sig = k256::ecdsa::Signature::from_scalars(r, s).unwrap(); + + verifying_key + .verify_prehash(&expected_hash, &sig) + .expect("signature must verify for long messages with varint"); + } + + #[test] + fn test_sign_message_varint_boundary() { + use k256::ecdsa::signature::hazmat::PrehashVerifier; + + let signer = ZcashSigner::mainnet(); + let privkey = test_privkey(); + let message = vec![0xAA; 253]; + + let result = signer.sign_message(&privkey, &message).unwrap(); + + // 253 is the boundary where single-byte CompactSize becomes invalid + let mut expected_data = Vec::new(); + expected_data.extend_from_slice(b"\x16Zcash Signed Message:\n"); + expected_data.push(0xFD); + expected_data.extend_from_slice(&253u16.to_le_bytes()); + expected_data.extend_from_slice(&message); + let expected_hash = Sha256::digest(Sha256::digest(&expected_data)); + + let signing_key = SigningKey::from_slice(&privkey).unwrap(); + let verifying_key = signing_key.verifying_key(); + let r: [u8; 32] = result.signature[..32].try_into().unwrap(); + let s: [u8; 32] = result.signature[32..64].try_into().unwrap(); + let sig = k256::ecdsa::Signature::from_scalars(r, s).unwrap(); + + verifying_key + .verify_prehash(&expected_hash, &sig) + .expect("signature must verify at varint boundary"); + } + + #[test] + fn test_deterministic() { + let signer = ZcashSigner::mainnet(); + let addr1 = signer.derive_address(&test_privkey()).unwrap(); + let addr2 = signer.derive_address(&test_privkey()).unwrap(); + assert_eq!(addr1, addr2); + } + + #[cfg(feature = "zcash-shielded")] + mod shielded_tests { + use super::*; + use crate::mnemonic::Mnemonic; + + const ABANDON_PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + fn test_seed() -> Vec { + let mnemonic = Mnemonic::from_phrase(ABANDON_PHRASE).unwrap(); + let seed = mnemonic.to_seed(""); + seed.expose().to_vec() + } + + #[test] + fn test_unified_address_mainnet() { + let signer = ZcashSigner::mainnet(); + let seed = test_seed(); + let address = signer.derive_unified_address(&seed, 0).unwrap(); + assert!( + address.starts_with("u1"), + "mainnet unified address should start with u1, got: {}", + address + ); + assert!( + address.len() > 50, + "unified address should be long (has multiple receivers), got len: {}", + address.len() + ); + } + + #[test] + fn test_unified_address_testnet() { + let signer = ZcashSigner::testnet(); + let seed = test_seed(); + let address = signer.derive_unified_address(&seed, 0).unwrap(); + assert!( + address.starts_with("utest1"), + "testnet unified address should start with utest1, got: {}", + address + ); + } + + #[test] + fn test_unified_address_deterministic() { + let signer = ZcashSigner::mainnet(); + let seed = test_seed(); + let addr1 = signer.derive_unified_address(&seed, 0).unwrap(); + let addr2 = signer.derive_unified_address(&seed, 0).unwrap(); + assert_eq!(addr1, addr2); + } + + #[test] + fn test_unified_address_different_accounts() { + let signer = ZcashSigner::mainnet(); + let seed = test_seed(); + let addr0 = signer.derive_unified_address(&seed, 0).unwrap(); + let addr1 = signer.derive_unified_address(&seed, 1).unwrap(); + assert_ne!(addr0, addr1, "different accounts must produce different addresses"); + } + + #[test] + fn test_needs_raw_seed() { + let signer = ZcashSigner::mainnet(); + assert!(signer.needs_raw_seed()); + } + + #[test] + fn test_derive_address_from_seed_matches() { + let signer = ZcashSigner::mainnet(); + let seed = test_seed(); + let addr_direct = signer.derive_unified_address(&seed, 0).unwrap(); + let addr_trait = signer.derive_address_from_seed(&seed, 0).unwrap(); + assert_eq!(addr_direct, addr_trait); + } + + #[test] + fn test_reject_short_seed() { + let signer = ZcashSigner::mainnet(); + let short_seed = vec![0u8; 16]; + let result = signer.derive_unified_address(&short_seed, 0); + assert!(result.is_err(), "should reject seed shorter than 32 bytes"); + } + } +} diff --git a/ows/crates/ows-signer/src/traits.rs b/ows/crates/ows-signer/src/traits.rs index 9ffe5eb3..3bdc7552 100644 --- a/ows/crates/ows-signer/src/traits.rs +++ b/ows/crates/ows-signer/src/traits.rs @@ -79,6 +79,32 @@ pub trait ChainSigner: Send + Sync { /// Returns the default BIP-44 derivation path template for this chain. fn default_derivation_path(&self, index: u32) -> String; + + /// Whether this chain requires the raw BIP-39 seed for address derivation + /// instead of a BIP-32/SLIP-10 derived key. + /// + /// Chains using non-standard HD derivation (e.g. Zcash's ZIP-32) override + /// this to return `true`, causing the wallet layer to pass the raw 64-byte + /// seed to [`derive_address_from_seed`] instead of a 32-byte derived key + /// to [`derive_address`]. + fn needs_raw_seed(&self) -> bool { + false + } + + /// Derive an on-chain address directly from a raw BIP-39 seed (64 bytes). + /// + /// Only called when [`needs_raw_seed`] returns `true`. Chains that use + /// non-BIP-44 derivation (e.g. ZIP-32 for Zcash unified addresses) + /// override this method. + fn derive_address_from_seed( + &self, + _seed: &[u8], + _account_index: u32, + ) -> Result { + Err(SignerError::AddressDerivationFailed( + "raw seed derivation not supported for this chain".into(), + )) + } } /// Errors that can occur during signing operations.