- Feature Name: (
zebra-client
) - Start Date: (2020-10-14)
- Design PR: ZcashFoundation/zebra#0000
- Zebra Issue: ZcashFoundation/zebra#0000
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:
Tree | Keys | Values |
---|---|---|
viewing_keys | IncomingViewingKey | String |
height_by_key | IncomingViewingKey | BE32(height) |
received_set_by_key | IncomingViewingKey | ? |
spend_set_by_key | IncomingViewingKey | ? |
nullifier_map_by_key | IncomingViewingKey | ? |
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 transaction 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
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 withoutzebra-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
- blazingly fast wallet startup, to match
-
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
- having
-
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.