Summary

The zebra-client crate handles client functionality. Client functionality is defined as all functionality related to a particular user's private data, in contrast to the other full node functionality which handles public chain state. This includes:

  • note and key management;
  • transaction generation;
  • a client component for zebrad that handles block chain scanning, with appropriate side-channel protections;
  • an RPC endpoint for zebrad that allows access to the client component;
  • Rust library code that implements basic wallet functionality;
  • a zebra-cli binary that wraps the wallet library and RPC queries in a command-line interface.

Client functionality is restricted to transparent and Sapling shielded transactions; Sprout shielded transactions are not supported. (Users should migrate to Sapling).

Motivation

We want to allow users to efficiently and securely send and receive funds via Zebra. One challenge unique to Zcash is block chain scanning: because shielded transactions reveal no metadata about the sender or receiver, users must scan the block chain for relevant transactions using viewing keys. This means that unlike a transparent blockchain with public transactions, a full node must have online access to viewing keys to scan the chain. This creates the risk of a privacy leak, because the node should not reveal which viewing keys it has access to.

Block chain scanning requires a mechanism that allows users to manage and store key material. This mechanism should also provide basic wallet functionality, so that users can send and receive funds without requiring third-party software.

To protect user privacy, this and all secret-dependent functionality should be strongly isolated from the rest of the node implementation. Care should be taken to protect against side channels that could reveal information about viewing keys. To make this isolation easier, all secret-dependent functionality is provided only by the zebra-client crate.

Definitions

  • client functionality: all functionality related to a particular user's private data, in contrast to other full node functionality which handles public chain state.

  • block chain scanning: the process of scanning the block chain for relevant transactions using a viewing key, as described in §4.19 of the protocol specification.

  • viewing key: Sapling shielded addresses support viewing keys, which represent the capability to decrypt transactions, as described in §3.1 and §4.2.2 of the protocol specification.

  • task: In this document, task refers specifically to a Tokio task. In brief, a task is a light weight, non-blocking unit of execution (green thread), similar to a Goroutine or Erlang process. Tasks execute independently and are scheduled co-operatively using explicit yield points. Tasks are executed on the Tokio runtime, which can either be single- or multi-threaded.

Guide-level explanation

There are two main parts of this functionality. The first is a Client component running as part of zebrad, and the second is a zebra-cli command-line tool.

The Client component is responsible for blockchain scanning. It maintains its own distinct sled database, which stores the viewing keys it uses to scan as well as the results of scanning. When a new block is added to the chain state, the Client component is notified asynchronously using a channel. For each Sapling shielded transaction in the block, the component attempts to perform trial decryption of that transaction's notes using each registered viewing key, as described in §4.19. If successful, decrypted notes are saved to the database.

The PING/REJECT attack demonstrates the importance of decoupling execution of normal node operations from secret-dependent operations. Zebra's network stack already makes it immune to those particular attacks, because each peer connection is executed in a different task. However, to eliminate this entire class of vulnerability, we execute the Client component in its own task, decoupled from the rest of the node functionality. In fact, each viewing key's scanning is performed independently, as described in more detail below, with an analysis of potential side-channels.

The second part is the zebra-cli command-line tool, which provides basic wallet functionality. This tool manages spending keys and addresses, and communicates with the Client component in zebrad to provide basic wallet functionality. Specifically, zebra-cli uses a distinct RPC endpoint to load viewing keys into zebrad and to query the results of block chain scanning. zebra-cli can then use the results of those queries to generate transactions and submit them to the network using zebrad.

This design upholds the principle of least authority by separating key material required for spending funds from the key material required for block chain scanning. This allows compartmentalization. For instance, a user could in principle run zebrad on a cloud VPS with only their viewing keys and store their spending keys on a laptop, or a user could run zebrad on a local machine and store their spending keys in a hardware wallet. Both of these use cases would require some additional tooling support, but are possible with this design.

Reference-level explanation

State notifications

We want a way to subscribe to updates from the state system via a channel. For the purposes of this RFC, these changes are in-flight, but in the future, these could be used for a push-based RPC mechanism.

Subscribers can subscribe to all state change notifications as they come in.

Currently the zebra_state::init() method returns a BoxService that allows you to make requests to the chain state. Instead, we would return a (BoxService, StateNotifications) tuple, where StateNotifications is a new structure initially defined as:

#[non_exhaustive]
pub struct StateNotifications {
  pub new_blocks: tokio::sync::watch::Receiver<Arc<Block>>,
}

Instead of making repeated polling requests to a state service to look for any new blocks, this channel will push new blocks to a consumer as they come in, for the consumer to use or discard at their discretion. This will be used by the client component described below. This will also be needed for gossiping blocks to other peers, as they are validated.

Online client component

This component maintains its own Sled tree. See RFC#0005 for more details on Sled.

We use the following Sled trees:

TreeKeysValues
viewing_keysIncomingViewingKeyString
height_by_keyIncomingViewingKeyBE32(height)
received_set_by_keyIncomingViewingKey?
spend_set_by_keyIncomingViewingKey?
nullifier_map_by_keyIncomingViewingKey?

See https://zips.z.cash/protocol/protocol.pdf#saplingscan

Zcash structures are encoded using ZcashSerialize/ZcashDeserialize.

This component runs inside zebrad. After incoming viewing keys are registered, it holds onto them in order to do blockchain scanning. The component keeps track of where it’s scanned to (TODO: per key?). Runs in its own separate task, in case it crashes, it’s not noticeable, and executes independently (but in the same process) of the normal node operation.

In the case of the client component that needs to do blockchain scanning and trial decryption, every valid block with non-coinbase transactions will need to be checked and its transactions trial-decrypted with registered incoming viewing keys to see if any notes have been received by the key's owner and if any notes have already been spent elsewhere.

RPC's

A specific set of privileged RPC endpoints:

  • Allows registering of incoming viewing keys with zebrad in order to do blockchain scanning
  • Allows querying of the results of that scanning, to get wallet balance, etc
  • Not authenticated to start (see 'Future possibilities')
  • Users can control access by controlling access to the privileged endpoint (ie via a firewall)

Support for sending tx's via non-privileged RPC endpoints, or via Stolon:

  • sendTransaction: once you author a transcation you can gossip it via any Zcash node, not just a specific instance of zebrad

Wallet functionality

  • Holds on to your spending keys so you can author transactions
  • Uses RPC methods to query the online client component inside zebrad about wallet balances

CLI binary

  • zebra-cli talks to the subcomponent running in zebrad
    • (can use servo/bincode to communicate with zebrad)
    • via the privileged (and possibly the unprivileged) RPC endpoints
    • can use cap-std to restrict filesystem and network access for zebra-client. See https://github.com/ZcashFoundation/zebra/issues/2340
    • can use the tui crate to render a terminal UI

Task isolation in Tokio

  • TODO: fill in
  • cooperative multitasking is fine, IF you cooperate
  • lots of tasks

Module Structure

zebra-client ( currently and empty stub) zebra-cli (does not exist yet) zebra-rfc? (exists as an empty stub, we way have zebra-cli communicate with zebra-client inside zebrad via an RPC method any/or a private IPC layer)

Test Plan

Drawbacks

Supporting a wallet assumes risk. Effort required to implement wallet functionality.

  • need to responsibly handle secret key material;
  • currently we only handle public data.

Rationale and alternatives

  • why have a separate RPC endpoint?

    • extra endpoints are cheap
    • allows segmentation by capability
    • alternative is error-prone after-the-fact ACLs like Tor control port filters
  • What is the impact of not doing this?

    • We can't send money with zebra alone.
    • rely on third party wallet software to send funds with zebra
      • we need to provide basic functionality within zebra's trust boundary, rather than forcing users to additionally trust 3p software
      • there are great 3p wallets, we want to integrate with them, just don't want to rely on them
  • What about the light client protocol?

    • does not address this use case, has different trust model (private lookup, no scanning)
    • we want our first client that interacts with zebrad to not have a long startup time, which a lightclient implementation would require
    • zebra-cli should be within the same trust and privacy boundary as the zebrad node it is interacting with
    • light client protocol as currently implemented requires stack assumptions such as protobufs and a hardcoded lightserver to talk to
  • What about having one database per key?

    • easy to reliably delete or backup all data related to a single key
    • might use slightly more space/CPU
    • slightly harder to delete all the keys

Unresolved questions

  • wait to fill this in until doing the detailed writeup.

Future possibilities

  • BlazeSync algorithm for fast syncing, like Zecwallet

  • mandatory sweeps for legacy keys

    • blazingly fast wallet startup, to match zebrad's blazingly fast sync
    • generate unified address from a new seed phrase (or one provided by the user)
    • user can just backup seed phrase rather than a set of private keys
    • handles arbitrary keys from zcashd and other wallets, even if they weren't generated from a seed phrase
    • handles Sprout funds without zebra-client having to support Sprout balances
    • startup is incredibly fast
      • sweep takes a few minutes to be confirmed
      • scanning the entire chain could take hours
      • if we know when the seed phrase was created, we can skip millions of blocks during scanning
    • sweeps can also be initiated by the user for non-linkability / performance / refresh
    • sweeps should handle the "block reward recipient" case where there are a lot of small outputs
    • initial release could support mandatory sweeps, and future releases could support legacy keys
  • split Client component into subprocess

    • this helps somewhat but the benefit is reduced by our preexisting memory safety, thanks to Rust
    • not meaningful without other isolation (need to restrict zebrad from accessing viewing keys on disk, etc)
    • could use cap-std to restrict filesystem and network access for zebra-client. See https://github.com/ZcashFoundation/zebra/issues/2340
    • instead of process isolation, maybe you actually want the Light Client Protocol, or something similar?
  • hardware wallet integration for zebra-cli

    • having zebra-cli allows us to do this
    • much higher security ROI than subprocess
    • very cool future feature
  • authenticate queries for a particular viewing key by proving knowledge of the viewing key (requires crypto). this could allow public access to the client endpoint

  • Use Unified Addresses only, no legacy addrs.