Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ A process that consumes a Validator's data and serves the light-client
gRPC protocol (zainod, or legacy lightwalletd).
_Avoid_: proxy, server

**Wallet**:
A light-client process the harness manages alongside the Validator and
Indexer: restored from a seed, synced through an Indexer, driven to
send and shield, and queried for balances and addresses. The harness
actuates every Wallet through one generic interface; each
implementation lives with its own binary (zcash-devtool, zingo-cli).
_Avoid_: client (the superseded name), lightclient

**Legacy stack**:
zcashd (Validator) and lightwalletd (Indexer). Opt-in only, scheduled for
simultaneous removal; neither is part of the harness's future.
Expand Down
18 changes: 17 additions & 1 deletion zcash_local_net/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
configured NU7 height instead of silently dropping it (the devtool
TOML gates `nu7` behind `zcash_unstable`), matching the zebrad
writer's no-silent-drop policy below.
- **Breaking** — the client layer is now the **Wallet abstraction**: the
`client` module is renamed to `wallet`, the `Client`/`ClientConfig`
traits to `Wallet`/`WalletConfig`, and `ClientError` to `WalletError`,
whose messages now name the operation rather than one binary. The
trait is the interface through which the harness actuates any wallet
implementation; implementations live with their binaries (the
zcash-devtool one remains in-tree for now, and a zingo-cli
implementation lands in the zingolib repository — see
`zingolib-wallet-impl-spec.md`).
- **Breaking** — the zebrad regtest config writer now **rejects activation
heights it cannot express** instead of silently dropping or rewriting
them: upgrades through Canopy must be `Some(1)` (the emitted config
Expand Down Expand Up @@ -87,7 +96,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `zingo_consensus::NetworkKind`: network identity without activation
heights, with `From<NetworkType>`/`From<&NetworkType>` conversions —
the config shape for components that must not be told heights.
- `client::WalletNetwork` and `client::ValidatorHeights`: the network a
- `LocalNet::launch_wallet::<W>(make_config)`: generic wallet
actuation — mints the `WalletNetwork` from the running Validator,
wires the Indexer connection, and launches any `Wallet`
implementation. `ValidatorHeights::activation_heights()` exposes the
reported schedule read-only, because foreign implementations must
serialize it into their own binaries' configs; construction remains
private to preserve the ADR 0003 provenance guarantee.
- `wallet::WalletNetwork` and `wallet::ValidatorHeights`: the network a
wallet is launched against, and regtest activation heights whose
provenance is a Validator query. `WalletNetwork::from_validator()` is
the only public constructor of `ValidatorHeights`, which makes ADR
Expand Down
13 changes: 7 additions & 6 deletions zcash_local_net/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ pub enum LaunchError {
}

/// Errors associated with driving wallet client operations
/// (run-to-completion subprocess invocations, see [`crate::client`]).
/// (see [`crate::wallet`]). Shared by every [`crate::wallet::Wallet`]
/// implementation, so messages name the operation, not one binary.
#[derive(thiserror::Error, Debug, Clone)]
pub enum ClientError {
pub enum WalletError {
/// The client binary could not be spawned at all
/// (PATH/`TEST_BINARIES_DIR`/permission problems).
#[error("zcash-devtool {operation} failed to spawn: {io_error}")]
#[error("wallet {operation} failed to spawn: {io_error}")]
SpawnFailed {
/// The wallet operation being attempted
operation: &'static str,
Expand All @@ -132,7 +133,7 @@ pub enum ClientError {
},
/// Writing to the child's stdin failed (used by `init`, which
/// receives the mnemonic on stdin).
#[error("zcash-devtool {operation}: writing to child stdin failed: {io_error}")]
#[error("wallet {operation}: writing to child stdin failed: {io_error}")]
StdinWriteFailed {
/// The wallet operation being attempted
operation: &'static str,
Expand All @@ -141,7 +142,7 @@ pub enum ClientError {
},
/// The operation subprocess exited non-zero.
#[error(
"zcash-devtool {operation} failed.\nExit status: {exit_status}\nStdout: {stdout}\nStderr: {stderr}"
"wallet {operation} failed.\nExit status: {exit_status}\nStdout: {stdout}\nStderr: {stderr}"
)]
OperationFailed {
/// The wallet operation being attempted
Expand All @@ -158,7 +159,7 @@ pub enum ClientError {
/// the contract-drift tripwire: it fires when the client binary's
/// output format changes out from under the harness's parsers.
#[error(
"zcash-devtool {operation} succeeded but its output could not be parsed: {reason}\nStdout: {stdout}"
"wallet {operation} succeeded but its output could not be parsed: {reason}\nStdout: {stdout}"
)]
UnexpectedOutput {
/// The wallet operation being attempted
Expand Down
21 changes: 19 additions & 2 deletions zcash_local_net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! # List of Managed Processes
//! - Zebrad
//! - Zainod
//! - zcash-devtool (wallet client; per-operation subprocess, see [`crate::client`])
//! - zcash-devtool (wallet; per-operation subprocess, see [`crate::wallet`])
//!
//! # Prerequisites
//!
Expand Down Expand Up @@ -42,7 +42,6 @@
//! See [`crate::LocalNet`].
//!

pub mod client;
pub mod config;
pub mod error;
pub mod indexer;
Expand All @@ -52,6 +51,7 @@ pub mod process;
pub mod rpc_client;
pub mod utils;
pub mod validator;
pub mod wallet;
pub mod zebra_rpc;

mod launch;
Expand Down Expand Up @@ -169,6 +169,23 @@ where
})
.await
}

/// Launch a wallet against this network, generically over the
/// wallet implementation `W`. The wallet's network is minted from
/// the running Validator ([`wallet::WalletNetwork::from_validator`],
/// the only source of regtest heights for a wallet config — ADR
/// 0003) and its server connection is wired to the Indexer.
/// `make_config` is one of the implementation's constructors, e.g.
/// `ZcashDevtoolConfig::faucet`.
pub async fn launch_wallet<W: wallet::Wallet>(
&self,
make_config: impl FnOnce(wallet::WalletNetwork) -> W::Config,
) -> Result<W, crate::error::WalletError> {
let network = wallet::WalletNetwork::from_validator(self.validator()).await;
let mut config = make_config(network);
wallet::WalletConfig::setup_indexer_connection(&mut config, self.indexer());
W::launch(config).await
}
}

impl<V> LocalNet<V, indexer::zainod::Zainod>
Expand Down
103 changes: 57 additions & 46 deletions zcash_local_net/src/client.rs → zcash_local_net/src/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
//! Module for the structs that represent and manage wallet client processes i.e. zcash-devtool.
//! The Wallet abstraction: the interface through which the harness
//! actuates wallets, generically over their implementations.
//!
//! Clients are the third kind of process this crate manages, alongside
//! Wallets are the third kind of process this crate manages, alongside
//! validators ([`crate::validator`]) and indexers ([`crate::indexer`]).
//! They differ structurally from both: the managed binary is not a
//! daemon. Each wallet operation (`init`, `sync`, `send`, …) is a
//! separate run-to-completion subprocess invocation against a persistent
//! wallet directory. There is no long-lived child handle to stop or to
//! probe for readiness; the managed state is the wallet directory
//! itself, created in a tempdir owned by the client struct and removed
//! when it drops.
//! This module defines only the contract — the [`Wallet`] and
//! [`WalletConfig`] traits and their supporting types. Implementations
//! live with their binaries: the zcash-devtool wallet is in
//! [`zcash_devtool`] (in-tree for now), and the zingo-cli wallet is
//! implemented in the zingolib repository against this trait. The
//! harness drives whichever implementation the caller names, e.g.
//! through `LocalNet::launch_wallet::<W>`.
//!
//! Clients speak the lightwalletd protocol (gRPC) to an indexer — they
//! Wallets speak the lightwalletd protocol (gRPC) to an indexer — they
//! never talk to the validator directly. Launch order is therefore
//! validator → indexer → client; [`ClientConfig::setup_indexer_connection`]
//! validator → indexer → wallet; [`WalletConfig::setup_indexer_connection`]
//! mirrors [`crate::indexer::IndexerConfig::setup_validator_connection`]
//! for wiring the client to a running indexer.
//! for wiring the wallet to a running indexer.
//!
//! Activation heights are the one exception to "never talk to the
//! validator": a regtest wallet needs the chain's schedule, the
Expand All @@ -23,20 +24,30 @@
//! on the wallet's behalf, and the type system enforces it — see
//! [`WalletNetwork`] and [`ValidatorHeights`].

use crate::{error::ClientError, indexer::Indexer};
use crate::{error::WalletError, indexer::Indexer};

/// Regtest activation heights whose provenance is a query of a running
/// Validator. The inner value has no public constructor and no public
/// accessor; the only way to obtain one is
/// [`WalletNetwork::from_validator`]. A wallet configured with these
/// heights is therefore guaranteed, at compile time, to have derived
/// them from the Validator (ADR 0003: the Validator is the single
/// source of truth for activation heights). Crate-internal tests may
/// construct the value directly to pin serialization offline, where no
/// chain exists for the heights to disagree with.
/// Validator. The inner value has no public constructor; the only way
/// to obtain one is [`WalletNetwork::from_validator`]. A wallet
/// configured with these heights is therefore guaranteed, at compile
/// time, to have derived them from the Validator (ADR 0003: the
/// Validator is the single source of truth for activation heights).
/// Crate-internal tests may construct the value directly to pin
/// serialization offline, where no chain exists for the heights to
/// disagree with.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatorHeights(pub(crate) zingo_consensus::ActivationHeights);

impl ValidatorHeights {
/// The schedule the Validator reported. Reading is public because
/// every [`Wallet`] implementation must serialize the heights into
/// its own binary's configuration; only *construction* is
/// restricted, since provenance — not secrecy — is the invariant.
pub fn activation_heights(&self) -> zingo_consensus::ActivationHeights {
self.0
}
}

/// The network a wallet client is launched against. Unlike
/// [`zingo_consensus::NetworkType`], the regtest variant cannot carry
/// caller-supplied heights: it demands a [`ValidatorHeights`], which
Expand Down Expand Up @@ -64,15 +75,15 @@ impl WalletNetwork {
}
}

/// Can offer specific functionality shared across configuration for all clients.
pub trait ClientConfig: std::fmt::Debug {
/// To receive the connection details of the indexer this client's
/// Configuration behavior every wallet implementation shares.
pub trait WalletConfig: std::fmt::Debug {
/// To receive the connection details of the indexer this
/// wallet will sync from and broadcast through.
fn setup_indexer_connection<I: Indexer>(&mut self, indexer: &I);
}

/// Which receiver of the wallet's unified address to emit from
/// [`Client::address`].
/// [`Wallet::address`].
///
/// A dedicated enum rather than [`zingo_consensus::MinerPool`], which
/// has no `Unified` variant — the wrong shape for "give me this
Expand All @@ -90,76 +101,76 @@ pub enum AddressReceiver {
Orchard,
}

/// Functionality for wallet client processes.
/// The interface through which the harness actuates a wallet.
///
/// The operation set mirrors what wallet integration suites (zaino's in
/// particular) drive between asserts: sync to tip, send, shield,
/// per-pool balance, address derivation, and rescan-from-scratch. All
/// operations run to completion before returning — callers can sequence
/// `act → mine → wait → assert` without additional synchronization.
pub trait Client: Sized {
/// A config struct for the client.
type Config: ClientConfig;
pub trait Wallet: Sized {
/// The configuration for this wallet implementation.
type Config: WalletConfig;

/// Create the wallet (restoring from the configured mnemonic and
/// birthday) and return the managed client. The configured indexer
/// birthday) and return the managed wallet. The configured indexer
/// must already be serving: wallet initialization fetches the chain
/// tip and the birthday tree state from it.
fn launch(config: Self::Config)
-> impl std::future::Future<Output = Result<Self, ClientError>>;
-> impl std::future::Future<Output = Result<Self, WalletError>>;

/// Scan the chain and sync the wallet to the indexer's tip.
fn sync(&self) -> impl std::future::Future<Output = Result<(), ClientError>>;
fn sync(&self) -> impl std::future::Future<Output = Result<(), WalletError>>;

/// Send `value_zats` zatoshis to `address` (transparent, sapling or
/// unified). Returns the txid of the broadcast transaction as a hex
/// string. The transaction is broadcast but NOT mined; mine a block
/// and [`Client::sync`] to confirm it.
/// and [`Wallet::sync`] to confirm it.
fn send(
&self,
address: &str,
value_zats: u64,
) -> impl std::future::Future<Output = Result<String, ClientError>>;
) -> impl std::future::Future<Output = Result<String, WalletError>>;

/// Shield transparent funds (including mature transparent coinbase)
/// into the orchard pool. Returns the txid of the broadcast
/// transaction as a hex string.
fn shield(&self) -> impl std::future::Future<Output = Result<String, ClientError>>;
fn shield(&self) -> impl std::future::Future<Output = Result<String, WalletError>>;

/// The wallet's view of its balance. Run [`Client::sync`] first;
/// The wallet's view of its balance. Run [`Wallet::sync`] first;
/// this reads the local wallet database without contacting the
/// indexer.
fn balance(&self) -> impl std::future::Future<Output = Result<WalletBalance, ClientError>>;
fn balance(&self) -> impl std::future::Future<Output = Result<WalletBalance, WalletError>>;

/// The requested `receiver` of the wallet's unified address, as an
/// encoded address string. Reads the local wallet database without
/// contacting the indexer.
fn address(
&self,
receiver: AddressReceiver,
) -> impl std::future::Future<Output = Result<String, ClientError>>;
) -> impl std::future::Future<Output = Result<String, WalletError>>;

/// The wallet's default unified address. Convenience for
/// [`Client::address`] with [`AddressReceiver::Unified`].
fn default_address(&self) -> impl std::future::Future<Output = Result<String, ClientError>> {
/// [`Wallet::address`] with [`AddressReceiver::Unified`].
fn default_address(&self) -> impl std::future::Future<Output = Result<String, WalletError>> {
self.address(AddressReceiver::Unified)
}

/// Node/indexer information reported by the configured server.
/// Contacts the indexer (the analogue of zingolib's `do_info`); a
/// smoke check that the wallet can reach and talk to its server.
fn get_info(&self) -> impl std::future::Future<Output = Result<GetInfo, ClientError>>;
fn get_info(&self) -> impl std::future::Future<Output = Result<GetInfo, WalletError>>;

/// Wipe the wallet state and re-restore from the stored mnemonic
/// and birthday, preserving account metadata. Equivalent to a
/// rescan from scratch; [`Client::sync`] afterwards to rebuild.
fn rescan(&self) -> impl std::future::Future<Output = Result<(), ClientError>>;
/// rescan from scratch; [`Wallet::sync`] afterwards to rebuild.
fn rescan(&self) -> impl std::future::Future<Output = Result<(), WalletError>>;
}

/// Node/indexer information from [`Client::get_info`].
/// Node/indexer information from [`Wallet::get_info`].
///
/// The field set is a frozen contract with the wallet binary: see
/// `client::zcash_devtool`'s get-info parser. `chain_tip_height` is the
/// `wallet::zcash_devtool`'s get-info parser. `chain_tip_height` is the
/// **server/node tip** the indexer reports, never the wallet's
/// locally-synced height (which, if ever surfaced, gets its own
/// explicitly-named field).
Expand All @@ -177,7 +188,7 @@ pub struct GetInfo {

/// A wallet balance snapshot, in zatoshis.
///
/// Spendable values are as reported by the client's configured
/// Spendable values are as reported by the wallet's configured
/// confirmations policy; immature or unconfirmed funds are included in
/// `total` but not in the per-pool spendable fields.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down
Loading
Loading