[](https://github.com/KarpelesLab/zanolib/actions/workflows/rust.yml)
# zanolib
Rust library for [Zano](https://zano.org/) cryptocurrency operations, including address parsing, offline transaction signing, deposit scanning, and zero-knowledge proof generation.
## Features
- **Address handling** — parse, create, and manipulate Zano addresses (standard, integrated, auditable)
- **Offline transaction signing** — sign transactions generated by a view-only simplewallet without exposing the spend key to the network
- **Deposit scanning** — detect incoming outputs for a wallet directly from a Zano daemon's JSON-RPC, decode amounts/assets, and recover integrated-address payment IDs (view key only)
- **Sending** — build, sign and broadcast transactions from scanned deposits (decoy ring selection, multi-asset, fee handling) entirely from the library
- **Threshold spend key (MPC)** — generate and hold the spend key as {t,n} shares across machines via FROST-ed25519 ([tsslib](https://github.com/KarpelesLab/tsslib-rs)); the secret is never reconstructed
- **Cryptographic primitives** — CLSAG-GGX ring signatures, Bulletproof+ range proofs, BGE asset surjection proofs, balance proofs
- **Serialization** — full binary serialization compatible with Zano's C++ implementation, plus the epee portable-storage codec used by the daemon's `.bin` endpoints
All cryptography comes from [purecrypto](https://github.com/KarpelesLab/purecrypto); there is no foreign code in the dependency tree.
## Install
```
cargo add zanolib
```
Cargo features (all on by default):
| feature | pulls in | provides |
|---------|----------|----------|
| `rpc` | `rsurl`, `base64` | the daemon client, deposit scanner and `sweep_to` |
| `mpc` | `tsslib` | the threshold spend key |
`cargo build --no-default-features` gives the pure offline core: addresses, wallets, parsing, signing and scanning.
## Module map
| module | contents |
|--------|----------|
| `crypto` | curve helpers, hash-to-point, key derivation, CLSAG-GGX, Bulletproof+, BGE |
| `base` | varints, the binary codec, transactions and their variant payloads |
| `epee` | the portable-storage codec used by the daemon's `.bin` endpoints |
| `proof` | transaction-wide balance, range and asset surjection proofs |
| `rpc` | JSON-RPC client and deposit scanner |
| `mpc` | threshold spend key via FROST |
Addresses, wallets, scanning and signing live at the crate root.
## Offline Signatures
Compatible Zano version: **2.1.0.382** – **2.1.19.477** (the ZC→ZC transaction format is unchanged across this range)
This library allows loading unsigned transactions produced by a view-only simplewallet and signing them offline. There are a few caveats:
- The unsigned transaction is a binary format **not** meant to be portable — it only works between specific versions of Zano. This library is tested against the versions above and may not work with newer versions. Blob files aren't versioned so structure changes cannot be detected automatically.
- For now this library only supports ZC→ZC transactions.
### Usage
```rust
use zanolib::rng::OsRng;
use zanolib::Wallet;
// Initialize a wallet from a securely stored spend secret.
// Pass flags = 1 for auditable wallets.
let wallet = Wallet::load_spend_secret(&secret, 0)?;
// Parse the unsigned transaction produced by simplewallet.
let ftp = wallet.parse_ftp(&unsigned_tx_blob)?;
// Inspect `ftp` to verify this is the transaction you want to sign.
// ...
// Sign it.
let finalized = wallet.sign(&mut OsRng, &ftp, None)?;
// Encrypt and write to disk for broadcast via the view-only wallet.
std::fs::write("zano_tx_signed", wallet.encrypt(&finalized)?)?;
```
## Deposit Scanning
zanolib can scan the chain for outputs paid to a wallet without running a local daemon — it fetches raw transactions over JSON-RPC from any Zano node and detects/decodes received outputs locally using only the **view secret key**. The caller stores each watched address together with the last block height it scanned, then resumes from there.
`Wallet::scan_tx` is the pure, offline core (it takes a parsed transaction and returns the owned outputs plus the decrypted payment ID). The `rpc` module adds a JSON-RPC client and a `Scanner` that walks confirmed block ranges.
```rust
use zanolib::rpc::{Deposit, Scanner};
use zanolib::Wallet;
// A view-only wallet is enough to detect deposits.
let wallet = view_wallet_data.load_view_wallet()?;
// "" = public modchain gateway; or a daemon base URL like "http://127.0.0.1:11211"
let scanner = Scanner::new(wallet, "");
// Resume from the last height you persisted, up to the current confirmed tip.
let (last, res) = scanner.scan_range(from_height, to_height, |d: Deposit| {
// d.out.amount, d.out.asset_id, d.out.is_native — what was received
// d.payment_id — integrated-address payment id (attributes the deposit to a user)
// d.tx_id, d.height, d.global_index — where it landed (global_index lets you spend it later)
credit_user(d);
Ok(())
});
res?;
// persist `last` as the new watermark; on restart re-scan a small tail to absorb reorgs
```
Notes:
- **Attribution**: give each user a distinct integrated address; `d.payment_id` maps a deposit back to the user. Per-wallet addresses work too (then payment IDs are simply absent).
- **Assets**: native ZANO and confidential assets are both decoded; `d.out.asset_id` / `d.out.is_native` identify which.
- **Confirmations / reorgs**: scan only finalized heights and re-scan a small tail on restart. Mempool (0-conf) scanning and spent/key-image tracking are not included.
## Sending
With a full wallet (spend secret), the library can build, sign and broadcast a transaction that spends scanned deposits — including decoy ring selection (via `getrandom_outs3.bin`), multi-asset transfers, and fee handling. `Client::sweep_to` sends a set of deposits to a recipient, deducting the fee from the native amount:
```rust
use zanolib::rpc::Client;
let wallet = Wallet::load_spend_secret(&secret, 0)?; // full wallet (can sign)
let client = Client::new(""); // "" = modchain gateway; or a daemon base URL
// deposits: gathered from scanning (see Deposit Scanning)
let res = client.sweep_to(
&wallet,
&deposits,
"Zx...recipient...", // destination address
10_000_000_000, // fee in native atomic units (0.01 ZANO)
true, // broadcast
)?;
// res.status == Some("OK") once accepted into the mempool
```
For finer control, `Wallet::build_transfer` assembles and signs an arbitrary set of `TransferInput`s and `TransferDest`s (you supply the decoy rings), returning a ready-to-serialize transaction. Native ZANO and confidential assets can be mixed in a single transaction; the fee is the native surplus and every non-native asset must balance.
### Roadmap / not yet implemented
The sending path currently spends an explicit, caller-provided set of deposits. Planned follow-ups:
- **Automatic coin/input selection** — pick which deposits to spend for a target amount (per asset), instead of requiring the caller to choose.
- **Change splitting** — produce change outputs (and sensible output splitting) rather than requiring inputs to sum exactly to outputs + fee.
- **Payment IDs on outgoing transactions** — attach an (encrypted) integrated-address payment ID when sending, not just recover it when scanning.
- **Gamma decoy distribution** — match Zano's `decoy_selection_generator` gamma curve for ring members; the current selection samples uniformly below the real output's index (valid, but not privacy-optimal).
## Threshold signing (MPC)
The spend key can be generated and held as a {t,n} threshold across machines using FROST-ed25519 (via [tsslib](https://github.com/KarpelesLab/tsslib-rs)), so the spend secret never exists whole on any single host. The `mpc` module converts a FROST group key into a Zano spend public key and address:
```rust
use zanolib::mpc;
// after running the frosttss DKG across N machines, each holds a tsslib::frosttss::Key:
let addr = mpc::address(&key.group_public_key, &view_public_key, 0)?;
```
The view key is **independent** of the threshold spend key: Zano's `view = keccak(spend)` derivation can't run under MPC because the spend secret is never reconstructed. Instead of supplying a random view key out of band, the committee derives a **deterministic** one from the shares with `ThresholdInputSigner::derive_view_secret()`, which runs tsslib's threshold key-image ceremony — a distributed PRF — under the identifier `ZANO_THRESHOLD_VIEWKEY`: each party contributes `W_i = λ_i·s_i·P` over a point `P` bound to the identifier and the group public key, the partials sum to `V = x·P`, and the view secret is `HashToScalar(identifier, V)` (Keccak-256, matching the rest of Zano). The shared secret never assembles, every party derives the identical value, and each partial carries a DLEQ proof so a wrong contribution is caught and its sender named. Partials travel point-to-point rather than by broadcast, since `t+1` of them would reconstruct `V`. The result is stable across runs *and across any qualifying committee*, so a threshold wallet has a fixed view key and therefore a fixed address. A Zano address is just `(spend_pub, view_pub)`, so this is valid on-chain — at the cost of seed-phrase recovery.
> The derived value differs from the removed Go implementation's `DeriveViewSecret()`, which hand-rolled the same construction with Zano's own hash-to-point, no DLEQ proofs and broadcast partials. Nothing to migrate: no threshold view key was ever deployed from the Go path.
Implemented and tested with local in-process peers (`{t,n}`, e.g. 2-of-3):
- **Distributed key generation → address** (`mpc::address`).
- **Threshold key image** — partial key images combine to the same key image for any signing subset (`mpc::partial_key_image`), as Zano's double-spend detection requires.
- **Deterministic threshold view key** — `ThresholdInputSigner::derive_view_secret()` derives a stable view secret from the shares (see above); tested to be identical across all parties, reproducible across runs, and unchanged when a different qualifying committee runs the ceremony.
- **Threshold CLSAG-GGX signing** — a two-round protocol computes the key image and layer-0 response from the shares; the spend secret is never reconstructed, and the resulting signature verifies with `crypto::verify_clsag_ggx`. It runs over **tsslib's own transport** (the same `MessageBroker` / `json_wrap` machinery `frosttss` uses), so it plugs into whatever transport you already use for FROST — the X-layer nonce and decoy responses are derived deterministically from the round-1 transcript so every party reaches the identical signature with no coordinator. (`ClsagParty` / `ClsagCoordinator` expose the same math for non-broker drivers.)
- **Wallet integration** — `Wallet::sign_with(rnd, ftp, one_time_key, signer)` signs a transfer through the `InputSigner` trait, so the spend-key operations (key image + CLSAG) are delegated to either a local secret (`LocalInputSigner`, the default `sign` path) or an MPC session. `mpc::ThresholdInputSigner` implements that trait over tsslib's broker: the committee jointly produces each input's key image and signature without reconstructing the spend secret. (Tested with all parties signing concurrently: the threshold key image matches a local signer's and the CLSAG verifies.)
Remaining: an end-to-end threshold-signed broadcast (all committee members running `sign_with` on a shared transaction — needs a shared one-time key and deterministic proof randomness so every party builds the identical tx, plus `frosttss` key import/reshare to put an existing wallet's secret under threshold).
## Verifying the port
This crate replaces a Go implementation, removed once the port was complete (it remains in the history, last present at `9269204`). The test suite pins the Rust code to that reference behaviour at three levels:
- **Known-answer vectors** (`tests/crypto_vectors.rs`) — ~1,900 vectors for `hash_to_point`, `hash_to_ec`, `hash_to_scalar`, key derivations and key images, carried over from the Go tests (which in turn track Zano's C++ code).
- **Real mainnet blobs** (`tests/onchain.rs`) — captured coinbase and transfer transactions must parse, re-serialize to the exact same bytes, and hash to their known transaction ids.
- **End-to-end signing** (`tests/sign_scan.rs`) — a committed `finalized_tx` fixture is decrypted, re-signed with a fixed RNG, and the resulting 7,285-byte transaction (prefix + CLSAG signatures + range, surjection and balance proofs) is compared against a stored fingerprint. That fingerprint is the transaction the Go implementation this crate replaces produced from the same inputs, so it pins the port to the behaviour that was in production.
```
cargo test --all-features
cargo test --no-default-features
```
## License
See [LICENSE](LICENSE) file.