feat(lite): vendor SDXL backend source; source deps from git.dragonx.is

Vendor the two local Rust crates that build the lite backend artifact into
third_party/silentdragonxlite/ (the qtlib C-ABI wrapper + the silentdragonxlitelib
core, with proto/res and all the lite-send fixes), and point
build-lite-backend-artifact.sh's default --backend-dir there, so the lite wallet
builds without the upstream SilentDragonXLite repo.

External build inputs are now only the Rust toolchain + git.dragonx.is + crates.io:
- the 6 librustzcash git deps point at the git.dragonx.is/DragonX/librustzcash
  mirror (pinned rev acff1444), not git.hush.is;
- the Sapling params are gitignored (not committed, no Git LFS) - the build fetches
  them from the git.dragonx.is/DragonX/zcash-params 'sapling-v1' release and verifies
  their SHA-256 before rust-embed bakes them in (ensure_sapling_params).

For fully offline builds, cargo vendor into lib/vendor/ and add a vendored-sources
redirect (vendor/ is gitignored; the script symlinks it into the prepared dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 15:06:42 -05:00
parent 3ce62326f9
commit 1393d9ae1a
29 changed files with 14322 additions and 2 deletions

4
.gitignore vendored
View File

@@ -47,3 +47,7 @@ docs/_archive/
# ed25519 release-signing keys — the secret key must NEVER be committed
*.ed25519.key
*.ed25519.pub.b64
# Lite-backend deps are fetched (or `cargo vendor`-ed locally for offline); not committed.
third_party/silentdragonxlite/lib/vendor/

View File

@@ -61,7 +61,7 @@ There is no per-test filtering — it is one binary that runs every assertion. T
Variants are selected with CMake options (set by `build.sh` flags), surfaced to C++ as compile definitions:
- `DRAGONX_BUILD_LITE` (`--lite`) → `DRAGONX_LITE_BUILD` define; renames the app to `ObsidianDragonLite` and excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).
- `DRAGONX_ENABLE_LITE_BACKEND` → links a real external lite backend. Requires `--lite`, link mode `imported`, ABI `sdxl-c-v1`, and a symbols inventory file (built by `scripts/build-lite-backend-artifact.sh`); CMake hard-fails if any required `litelib_*` symbol is missing.
- `DRAGONX_ENABLE_LITE_BACKEND` → links a real external lite backend. Requires `--lite`, link mode `imported`, ABI `sdxl-c-v1`, and a symbols inventory file (built by `scripts/build-lite-backend-artifact.sh`); CMake hard-fails if any required `litelib_*` symbol is missing. The backend **source is vendored in-tree** at `third_party/silentdragonxlite/` — the `qtlib` C-ABI wrapper (`lib/`, produces `libsilentdragonxlite.a`) and the `silentdragonxlitelib` core (`silentdragonxlite-cli/lib/`, with `proto/` + `res/`). `build-lite-backend-artifact.sh` defaults `--backend-dir` there, so the lite wallet builds **without** the upstream SilentDragonXLite repo. External build inputs are limited to the **Rust toolchain (rustc/cargo 1.63)** plus two project-controlled sources on `git.dragonx.is`: the librustzcash crates come from the mirror `git.dragonx.is/DragonX/librustzcash` (the 6 `git =` deps in the core `Cargo.toml`, pinned to rev `acff1444…`), and the **Sapling params are not committed** (gitignored) — the build fetches them from the `git.dragonx.is/DragonX/zcash-params` release `sapling-v1` and verifies their SHA-256 before rust-embed bakes them in (`ensure_sapling_params`; override the URL with `SAPLING_PARAMS_BASE_URL`). Other crate deps come from crates.io. For a fully offline build, `cargo vendor` into `third_party/silentdragonxlite/lib/vendor/` and add a `vendored-sources` redirect to `lib/.cargo/config.toml` (the build script symlinks `vendor/` into its prepared dir if present); `vendor/` is gitignored.
- `DRAGONX_ENABLE_CHAT``DRAGONX_ENABLE_CHAT` define gating the chat module.
Guard full-node-only code paths with `#if DRAGONX_LITE_BUILD` / chat code with `DRAGONX_ENABLE_CHAT`.

View File

@@ -7,7 +7,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ABI_VERSION="sdxl-c-v1"
LINK_MODE="imported"
BACKEND_DIR="$PROJECT_ROOT/external/SilentDragonXLite/lib"
BACKEND_DIR="$PROJECT_ROOT/third_party/silentdragonxlite/lib"
BACKEND_SOURCE_DIR=""
BUILD_BACKEND_DIR=""
BACKEND_DEPENDENCY_DIR=""
@@ -314,6 +314,33 @@ validate_backend_dependency_source() {
fi
}
# Ensure the Sapling proving params are present in the core crate (rust-embed bakes them in at build
# time). They are the fixed Zcash trusted-setup output — not buildable — so fetch + verify them from
# git.dragonx.is when absent. Override the source with SAPLING_PARAMS_BASE_URL.
SAPLING_PARAMS_BASE_URL="${SAPLING_PARAMS_BASE_URL:-https://git.dragonx.is/DragonX/zcash-params/releases/download/sapling-v1}"
ensure_sapling_params() {
local dir="$1"
[[ -n "$dir" ]] || return 0
mkdir -p "$dir"
local specs=(
"sapling-spend.params:8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
"sapling-output.params:2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
)
local spec name want path got
for spec in "${specs[@]}"; do
name="${spec%%:*}"; want="${spec##*:}"; path="$dir/$name"
if [[ -f "$path" ]] && [[ "$(compute_sha256 "$path")" == "$want" ]]; then
info "sapling param present and verified: $name"
continue
fi
info "fetching $name from $SAPLING_PARAMS_BASE_URL"
curl -fsSL "$SAPLING_PARAMS_BASE_URL/$name" -o "$path" || die "failed to download sapling param: $name"
got="$(compute_sha256 "$path")"
[[ "$got" == "$want" ]] || { rm -f "$path"; die "sapling param $name sha256 mismatch (got $got, want $want)"; }
info "downloaded and verified $name"
done
}
prepare_backend_source() {
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
@@ -348,6 +375,9 @@ prepare_backend_source() {
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
# "vendor" relative to the build root, so expose it inside the prepared root too.
[[ -d "$BACKEND_SOURCE_DIR/vendor" ]] && ln -s "$BACKEND_SOURCE_DIR/vendor" "$prepared_root/vendor"
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
@@ -489,6 +519,8 @@ build_with_cargo() {
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
fi
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] && ensure_sapling_params "$BACKEND_DEPENDENCY_DIR/zcash-params"
local cargo_cmd=(cargo build --locked --lib --release)
if [[ -n "$RUST_TARGET" ]]; then
cargo_cmd+=(--target "$RUST_TARGET")

View File

@@ -0,0 +1,2 @@
[net]
git-fetch-with-cli = true

View File

@@ -0,0 +1,2 @@
/target/

2751
third_party/silentdragonxlite/lib/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
[package]
name = "qtlib"
version = "0.1.0"
authors = ["zecwallet", "The Hush Developers"]
edition = "2018"
[lib]
name = "silentdragonxlite"
crate-type = ["staticlib"]
[dependencies]
libc = "0.2.58"
lazy_static = "1.4.0"
blake3 = "0.3.4"
silentdragonxlitelib = { path = "../silentdragonxlite-cli/lib" }
socket2 = "0.3.11"

View File

@@ -0,0 +1,28 @@
ifeq ($(shell uname),Darwin)
EXT := dylib
CFLAGS := "-mmacosx-version-min=10.11"
else
EXT := a
CFLAGS :=
endif
PWD := $(shell pwd)
all: release
winrelease: target/x86_64-pc-windows-gnu/release/silentdragonxlite.lib
target/x86_64-pc-windows-gnu/release/silentdragonxlite.lib: src/lib.rs Cargo.toml
SODIUM_LIB_DIR="$(PWD)/libsodium-mingw/" cargo build --lib --release --target x86_64-pc-windows-gnu
release: target/release/silentdragonxlite.$(EXT)
debug: target/debug/silentdragonxlite.$(EXT)
target/release/silentdragonxlite.$(EXT): src/lib.rs Cargo.toml
LIBS="" CFLAGS=$(CFLAGS) cargo build --lib --release
target/debug/silentdragonxlite.$(EXT): src/lib.rs Cargo.toml
LIBS="" CFLAGS=$(CFLAGS) cargo build --lib
clean:
rm -rf target

Binary file not shown.

View File

@@ -0,0 +1,29 @@
#ifndef _hush_PAPER_RUST_H
#define _hush_PAPER_RUST_H
#ifdef __cplusplus
extern "C" {
#endif
extern bool litelib_wallet_exists (const char* chain_name);
extern char * litelib_initialize_new (bool dangerous, const char* server);
extern char * litelib_initialize_new_from_phrase
(bool dangerous, const char* server, const char* seed,
unsigned long long birthday, unsigned long long number,
bool overwrite);
extern char * litelib_initialize_existing (bool dangerous,const char* server);
extern char * litelib_execute (const char* s, const char* args);
extern void litelib_rust_free_string (char* s);
extern char * blake3_PW (char* pw);
extern bool litelib_check_server_online (const char* server);
extern void litelib_shutdown (void);
#ifdef __cplusplus
}
#endif
// This is a function implemented in connection.cpp that will process a string response from
// the litelib and turn into into a QString in a memory-safe way.
QString litelib_process_response(char* resp);
#endif

View File

@@ -0,0 +1,328 @@
#[macro_use]
extern crate lazy_static;
use libc::{c_char};
use std::ffi::{CStr, CString};
use std::sync::{Mutex, Arc};
use std::cell::RefCell;
use std::ptr;
use std::panic;
use silentdragonxlitelib::{commands, lightclient::{LightClient, LightClientConfig}};
/// Helper to create a CString, replacing null bytes to avoid panics
fn safe_cstring(s: &str) -> CString {
let cleaned: String = s.replace('\0', "");
CString::new(cleaned).unwrap_or_else(|_| CString::new("Error: failed to create CString").unwrap())
}
/// Helper to create an error CString
fn error_cstring(msg: &str) -> *mut c_char {
safe_cstring(&format!("Error: {}", msg)).into_raw()
}
// We'll use a MUTEX to store a global lightclient instance,
// so we don't have to keep creating it. We need to store it here, in rust
// because we can't return such a complex structure back to C++
lazy_static! {
static ref LIGHTCLIENT: Mutex<RefCell<Option<Arc<LightClient>>>> = Mutex::new(RefCell::new(None));
}
// Check if there is an existing wallet
#[no_mangle]
pub extern fn litelib_wallet_exists(chain_name: *const c_char) -> bool {
let chain_name_str = unsafe {
assert!(!chain_name.is_null());
CStr::from_ptr(chain_name).to_string_lossy().into_owned()
};
let config = LightClientConfig::create_unconnected(chain_name_str, None);
println!("Wallet exists: {}", config.wallet_exists());
config.wallet_exists()
}
//////hash blake3
#[no_mangle]
pub extern fn blake3_PW(pw: *const c_char) -> *mut c_char{
let passwd = unsafe {
assert!(!pw.is_null());
CStr::from_ptr(pw).to_string_lossy().into_owned()
};
let data = passwd.as_bytes();
// Hash an input all at once.
let hash1 = blake3::hash(data).to_hex();
// This is sensitive metadata, do not log it to stdout
//println!("\nBlake3 Hash: {}", hash1);
println!("\nBlake3 Hash calculated");
//let sttring = CString::new(hash1).unwrap();
let e_str = CString::new(format!("{}", hash1)).unwrap();
return e_str.into_raw();
}
/// Create a new wallet and return the seed for the newly created wallet.
#[no_mangle]
pub extern fn litelib_initialize_new(dangerous: bool,server: *const c_char) -> *mut c_char {
let server_str = unsafe {
assert!(!server.is_null());
CStr::from_ptr(server).to_string_lossy().into_owned()
};
let server = LightClientConfig::get_server_or_default(Some(server_str));
let (config, latest_block_height) = match LightClientConfig::create(server, dangerous) {
Ok((c, h)) => (c, h),
Err(e) => {
let e_str = CString::new(format!("Error: {}", e)).unwrap();
return e_str.into_raw();
}
};
let lightclient = match LightClient::new(&config, latest_block_height) {
Ok(l) => l,
Err(e) => {
let e_str = CString::new(format!("Error: {}", e)).unwrap();
return e_str.into_raw();
}
};
// Initialize logging
let _ = lightclient.init_logging();
let seed = match lightclient.do_seed_phrase() {
Ok(s) => s.dump(),
Err(e) => {
let e_str = CString::new(format!("Error: {}", e)).unwrap();
return e_str.into_raw();
}
};
let lc = Arc::new(lightclient);
match LightClient::start_mempool_monitor(lc.clone()) {
Ok(_) => {println!("Starting Mempool")},
Err(e) => {
println!("Couldnt start mempool {}", e)
}
}
match LIGHTCLIENT.lock() {
Ok(l) => { l.replace(Some(lc)); },
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
};
// Return the wallet's seed
let s_str = safe_cstring(&seed);
return s_str.into_raw();
}
/// Restore a wallet from the seed phrase
#[no_mangle]
pub extern "C" fn litelib_initialize_new_from_phrase(dangerous: bool, server: *const c_char,
seed: *const c_char, birthday: u64, number: u64, overwrite: bool) -> *mut c_char {
if server.is_null() || seed.is_null() {
println!("Server or seed is null");
return ptr::null_mut();
}
let server_str = unsafe {
CStr::from_ptr(server).to_string_lossy().into_owned()
};
let seed_str = unsafe {
CStr::from_ptr(seed).to_string_lossy().into_owned()
};
//println!("Initializing with server: {}, seed: {}", server_str, seed_str);
// Shut down the existing client if one is running, to stop background threads
if overwrite {
let old_lc = match LIGHTCLIENT.lock() {
Ok(l) => l.borrow().clone(),
Err(poisoned) => poisoned.into_inner().borrow().clone(),
};
if let Some(lc) = old_lc {
lc.shutdown();
}
}
let server = LightClientConfig::get_server_or_default(Some(server_str));
let (config, _latest_block_height) = match LightClientConfig::create(server, dangerous) {
Ok((c, h)) => {
println!("Config created successfully");
(c, h)
},
Err(e) => {
println!("Error creating config: {}", e);
let e_str = CString::new(format!("Error: {}", e)).unwrap_or_else(|_| CString::new("Error creating CString").unwrap());
return e_str.into_raw();
}
};
let lightclient = match LightClient::new_from_phrase(seed_str, &config, birthday, number, overwrite) {
Ok(l) => {
println!("LightClient created successfully");
l
},
Err(e) => {
println!("Error creating LightClient: {}", e);
let e_str = CString::new(format!("Error: {}", e)).unwrap_or_else(|_| CString::new("Error creating CString").unwrap());
return e_str.into_raw();
}
};
// Initialize logging
let _ = lightclient.init_logging();
let lc = Arc::new(lightclient);
match LightClient::start_mempool_monitor(lc.clone()) {
Ok(_) => println!("Starting Mempool"),
Err(e) => println!("Could not start mempool: {}", e)
}
match LIGHTCLIENT.lock() {
Ok(l) => { l.replace(Some(lc)); },
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
};
let c_str = safe_cstring("OK");
return c_str.into_raw();
}
// Initialize a new lightclient and store its value
#[no_mangle]
pub extern fn litelib_initialize_existing(dangerous: bool, server: *const c_char) -> *mut c_char {
let server_str = unsafe {
assert!(!server.is_null());
CStr::from_ptr(server).to_string_lossy().into_owned()
};
let server = LightClientConfig::get_server_or_default(Some(server_str));
let (config, _latest_block_height) = match LightClientConfig::create(server,dangerous) {
Ok((c, h)) => (c, h),
Err(e) => {
let e_str = CString::new(format!("Error: {}", e)).unwrap();
return e_str.into_raw();
}
};
let lightclient = match LightClient::read_from_disk(&config) {
Ok(l) => l,
Err(e) => {
let e_str = CString::new(format!("Error: {}", e)).unwrap();
return e_str.into_raw();
}
};
// Initialize logging
let _ = lightclient.init_logging();
let lc = Arc::new(lightclient);
match LightClient::start_mempool_monitor(lc.clone()) {
Ok(_) => {println!("Starting Mempool")},
Err(e) => {
println!("Couldnt start mempool {}",e)
}
}
match LIGHTCLIENT.lock() {
Ok(l) => { l.replace(Some(lc)); },
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
};
let c_str = safe_cstring("OK");
return c_str.into_raw();
}
#[no_mangle]
pub extern fn litelib_execute(cmd: *const c_char, args: *const c_char) -> *mut c_char {
let result = panic::catch_unwind(|| {
let cmd_str = unsafe {
assert!(!cmd.is_null());
CStr::from_ptr(cmd).to_string_lossy().into_owned()
};
let arg_str = unsafe {
assert!(!args.is_null());
CStr::from_ptr(args).to_string_lossy().into_owned()
};
let resp: String;
{
let lightclient: Arc<LightClient>;
{
let lc = match LIGHTCLIENT.lock() {
Ok(l) => l,
Err(poisoned) => poisoned.into_inner(),
};
if lc.borrow().is_none() {
return error_cstring("Light Client is not initialized");
}
lightclient = lc.borrow().as_ref().unwrap().clone();
};
let args = if arg_str.is_empty() { vec![] } else { vec![arg_str.as_ref()] };
resp = commands::do_user_command(&cmd_str, &args, lightclient.as_ref()).clone();
};
safe_cstring(&resp).into_raw()
});
match result {
Ok(ptr) => ptr,
Err(_) => error_cstring("Rust panic in litelib_execute"),
}
}
// Check is Server Connection is fine
#[no_mangle]
pub extern "C" fn litelib_check_server_online(server: *const c_char) -> bool {
let server_str = unsafe {
assert!(!server.is_null());
CStr::from_ptr(server).to_string_lossy().into_owned()
};
let server = LightClientConfig::get_server_or_default(Some(server_str));
let result = LightClientConfig::create(server, false);
match result {
Ok(_) => true,
Err(_) => false,
}
}
/// Cleanly shut down the light client, stopping mempool monitor threads.
/// Must be called before exit to prevent hangs.
#[no_mangle]
pub extern "C" fn litelib_shutdown() {
let lc_option = match LIGHTCLIENT.lock() {
Ok(l) => l.borrow().clone(),
Err(poisoned) => poisoned.into_inner().borrow().clone(),
};
if let Some(lc) = lc_option {
lc.shutdown();
}
}
/**
* Callers that receive string return values from other functions should call this to return the string
* back to rust, so it can be freed. Failure to call this function will result in a memory leak
*/
#[no_mangle]
pub extern fn litelib_rust_free_string(s: *mut c_char) {
unsafe {
if s.is_null() { return }
CString::from_raw(s)
};
}

View File

@@ -0,0 +1,80 @@
[package]
name = "silentdragonxlitelib"
version = "0.1.0"
edition = "2018"
[features]
default = ["embed_params"]
embed_params = []
[dependencies]
base58 = "0.1.0"
bs58 = { version = "0.2", features = ["check"] }
log = "0.4"
log4rs = "0.8.3"
dirs = "2.0.2"
http = "0.2"
hex = "0.3"
protobuf = "2"
byteorder = "1"
json = "0.12.0"
tiny-bip39 = "0.6.2"
secp256k1 = "=0.15.0"
sha2 = "0.8.0"
ripemd160 = "0.8.0"
lazy_static = "1.2.0"
rust-embed = { version = "5.1.0", features = ["debug-embed"] }
rand = "0.7.2"
sodiumoxide = "0.2.5"
ring = "0.16.9"
libflate = "0.1"
subtle = "2"
threadpool = "1.8.0"
num_cpus = "1.13.0"
tonic = { version = "0.1.1", features = ["tls", "tls-roots"] }
bytes = "0.4"
prost = "0.6"
prost-types = "0.6"
tokio = { version = "0.2", features = ["rt-threaded", "time", "stream", "fs", "macros", "uds", "full"] }
tokio-rustls = { version = "0.12.1", features = ["dangerous_configuration"] }
webpki = "0.21.0"
webpki-roots = "0.18.0"
[dependencies.bellman]
git = "https://git.dragonx.is/DragonX/librustzcash.git"
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
default-features = false
features = ["groth16"]
[dependencies.pairing]
git = "https://git.dragonx.is/DragonX/librustzcash.git"
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
[dependencies.zcash_client_backend]
git = "https://git.dragonx.is/DragonX/librustzcash.git"
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
default-features = false
[dependencies.zcash_primitives]
git = "https://git.dragonx.is/DragonX/librustzcash.git"
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
default-features = false
features = ["transparent-inputs"]
[dependencies.zcash_proofs]
git = "https://git.dragonx.is/DragonX/librustzcash.git"
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
default-features = false
[dependencies.ff]
git = "https://git.dragonx.is/DragonX/librustzcash.git"
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
features = ["ff_derive"]
[build-dependencies]
tonic-build = "0.1.1"
[dev-dependencies]
tempdir = "0.3.7"

View File

@@ -0,0 +1,13 @@
// Copyright The Hush Developers 2019-2022
// Released under the GPLv3
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(false)
.compile(
&["proto/service.proto", "proto/compact_formats.proto"],
&["proto"],
)?;
println!("cargo:rerun-if-changed=proto/service.proto");
Ok(())
}

View File

@@ -0,0 +1,65 @@
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = "lightwalletd/walletrpc";
option swift_prefix = "";
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
// bytes fields of hashes are in canonical little-endian format.
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
// 1. Detect a payment to your shielded Sapling address
// 2. Detect a spend of your shielded Sapling notes
// 3. Update your witnesses to generate new Sapling spend proofs.
message CompactBlock {
uint32 protoVersion = 1; // the version of this wire format, for storage
uint64 height = 2; // the height of this block
bytes hash = 3; // the ID (hash) of this block, same as in block explorers
bytes prevHash = 4; // the ID (hash) of this block's predecessor
uint32 time = 5; // Unix epoch time when the block was mined
bytes header = 6; // (hash, prevHash, and time) OR (full header)
repeated CompactTx vtx = 7; // compact transactions from this block
}
message CompactTx {
// Index and hash will allow the receiver to call out to chain
// explorers or other data structures to retrieve more information
// about this transaction.
uint64 index = 1; // the index within the full block
bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers
// The transaction fee: present if server can provide. In the case of a
// stateless server and a transaction with transparent inputs, this will be
// unset because the calculation requires reference to prior transactions.
// in a pure-Sapling context, the fee will be calculable as:
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
uint32 fee = 3;
repeated CompactSaplingSpend spends = 4;
repeated CompactSaplingOutput outputs = 5;
}
// CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash
// protocol specification.
message CompactSaplingSpend {
bytes nf = 1; // nullifier (see the Zcash protocol specification)
}
// output is a Sapling Output Description as described in section 7.4 of the
// Zcash protocol spec. Total size is 948.
message CompactSaplingOutput {
bytes cmu = 1; // note commitment u-coordinate
bytes epk = 2; // ephemeral public key
bytes ciphertext = 3; // first 52 bytes of ciphertext
}
/*
message CompactSpend {
bytes nf = 1; // nullifier (see the Zcash protocol specification)
}
message CompactOutput {
bytes cmu = 1; // note commitment u-coordinate
bytes epk = 2; // ephemeral public key
bytes ciphertext = 3; // first 52 bytes of ciphertext
}
*/

View File

@@ -0,0 +1,170 @@
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = "lightwalletd/walletrpc";
option swift_prefix = "";
import "compact_formats.proto";
// A BlockID message contains identifiers to select a block: a height or a
// hash. If the hash is present it takes precedence.
message BlockID {
uint64 height = 1;
bytes hash = 2;
}
// BlockRange technically allows ranging from hash to hash etc but this is not
// currently intended for support, though there is no reason you couldn't do
// it. Further permutations are left as an exercise.
message BlockRange {
BlockID start = 1;
BlockID end = 2;
}
// A TxFilter contains the information needed to identify a particular
// transaction: either a block and an index, or a direct transaction hash.
message TxFilter {
BlockID block = 1;
uint64 index = 2;
bytes hash = 3;
}
// RawTransaction contains the complete transaction data. It also optionally includes
// the block height in which the transaction was included
message RawTransaction {
bytes data = 1;
uint64 height = 2;
}
message SendResponse {
int32 errorCode = 1;
string errorMessage = 2;
}
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
message ChainSpec {}
message Empty {}
message LightdInfo {
string version = 1;
string vendor = 2;
bool taddrSupport = 3;
string chainName = 4;
uint64 saplingActivationHeight = 5;
string consensusBranchId = 6; // This should really be u32 or []byte, but string for readability
uint64 blockHeight = 7;
uint64 difficulty = 8;
uint64 longestchain = 9;
uint64 notarized = 10;
}
message Coinsupply {
string result = 1;
string coin = 2;
uint64 height = 3;
uint64 supply = 4;
uint64 zfunds = 5;
uint64 total = 6;
}
message TransparentAddress {
string address = 1;
}
message TransparentAddressBlockFilter {
string address = 1;
BlockRange range = 2;
}
message Address {
string address = 1;
}
message AddressList {
repeated string addresses = 1;
}
message Balance {
int64 valueZat = 1;
}
message Exclude {
repeated bytes txid = 1;
}
// The TreeState is derived from the Hush getblockmerkletree rpc.
// https://faq.hush.is/rpc/getblockmerkletree.html
message TreeState {
string network = 1; // "main" or "test"
uint64 height = 2; // block height
string hash = 3; // block id
uint32 time = 4; // Unix epoch time when the block was mined
string saplingTree = 5; // sapling commitment tree state
}
// Results are sorted by height, which makes it easy to issue another
// request that picks up from where the previous left off.
message GetAddressUtxosArg {
repeated string addresses = 1;
uint64 startHeight = 2;
uint32 maxEntries = 3; // zero means unlimited
}
message GetAddressUtxosReply {
string address = 6;
bytes txid = 1;
int32 index = 2;
bytes script = 3;
int64 valueZat = 4;
uint64 height = 5;
}
message GetAddressUtxosReplyList {
repeated GetAddressUtxosReply addressUtxos = 1;
}
service CompactTxStreamer {
// Return the height of the tip of the best chain
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
// Return the compact block corresponding to the given block identifier
rpc GetBlock(BlockID) returns (CompactBlock) {}
// Return a list of consecutive compact blocks
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
// Return the requested full (not compact) transaction (as from zcashd)
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
// Submit the given transaction to the Zcash network
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
// Return the txids corresponding to the given t-address within the given block range
rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
// wrapper for GetTaddressTxids
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
rpc GetTaddressBalance(AddressList) returns (Balance) {}
rpc GetTaddressBalanceStream(stream Address) returns (Balance) {}
// Return the compact transactions currently in the mempool; the results
// can be a few seconds out of date. If the Exclude list is empty, return
// all transactions; otherwise return all *except* those in the Exclude list
// (if any); this allows the client to avoid receiving transactions that it
// already has (from an earlier call to this rpc). The transaction IDs in the
// Exclude list can be shortened to any number of bytes to make the request
// more bandwidth-efficient; if two or more transactions in the mempool
// match a shortened txid, they are all sent (none is excluded). Transactions
// in the exclude list that don't exist in the mempool are ignored.
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
// Return a stream of current Mempool transactions. This will keep the output stream open while
// there are mempool transactions. It will close the returned stream when a new block is mined.
rpc GetMempoolStream(Empty) returns (stream RawTransaction) {}
// GetTreeState returns the note commitment tree state corresponding to the given block.
// See section 3.7 of the Zcash protocol specification. It returns several other useful
// values also (even though they can be obtained using GetBlock).
// The block can be specified by either height or hash.
rpc GetTreeState(BlockID) returns (TreeState) {}
rpc GetLatestTreeState(Empty) returns (TreeState) {}
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
// Return information about this lightwalletd instance and the blockchain
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
// Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production)
// rpc Ping(Duration) returns (PingResponse) {}
rpc GetCoinsupply(Empty) returns (Coinsupply) {}
}

View File

@@ -0,0 +1,65 @@
-----BEGIN CERTIFICATE-----
MIIFlTCCA32gAwIBAgIUCU7sjrIbYfA+bc2qWlyUo0dCU7swDQYJKoZIhvcNAQEL
BQAwWjELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAoM
BEh1c2gxDTALBgNVBAsMBEh1c2gxGDAWBgNVBAMMD2xpdGUubXlodXNoLm9yZzAe
Fw0xOTEyMDIxNDUyMzBaFw0yMDEyMDExNDUyMzBaMFoxCzAJBgNVBAYTAlVTMRMw
EQYDVQQIDApTb21lLVN0YXRlMQ0wCwYDVQQKDARIdXNoMQ0wCwYDVQQLDARIdXNo
MRgwFgYDVQQDDA9saXRlLm15aHVzaC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQDhd3SLJuGQ3ivUWle6+Est+qNBghf2vMkcgj9NjaxeMjSMLPVq
Mxt0j/mJe3+z767yXbiBUKnaQyb7OcWtxUN3PusGmqnMAUuy/tdu9h+2ScYKThh2
JHQNdyN1y4c7sFbmntpMIIqm6/v95UXnnStQ+VBlS2/IhLYTgW31DEIiTpyx4jjW
xY+QD2+mqf4sSDm4Yq/r3Cxp6YWufEbhkXiHcF15JPk1d6jzkOkcjCJJCqwRMJ+5
60q31S1W0Ud/L7AqkOhAKFHmORfCXM0ae4Rive/ZgM688KYIXA9kQzA6ZMdD7VIL
4q4IoP1ZjlPhosFoUFB6lHORYp15+Gu43jbC2/SUPWJQbJ1XusjxysqngyJ53/Va
MN/iWhOmqBXjx1SqkyIV4W56GDezxT1MhM5zSSKgEHePyFzkGNYasEeHa1/hZoz+
zKG1oGLlMQe5TtI3AMZMfLz6t8qtRB+k+XW988mHJZ7BYOjW3KvdN16SOYdFF6K+
86MAQ8rNPgcTsnclhmDdjh7+PhQpkF3uqF1EeVTzb03s77Cx6nDc9GCnpXqg8tkE
HnJD0WFIXA29PCjWyebuksMBRahekYDR0kn8O0Km/eFAprH3v4qoSc1JLNJR2G20
eHVDnNFnR6QdwlM9+39NYUhJV43aj28wx1m0FXI+dSklblk4hkbGPPbFjQIDAQAB
o1MwUTAdBgNVHQ4EFgQUo5TCdrNgopYbxQSzSPFSlyCtE6YwHwYDVR0jBBgwFoAU
o5TCdrNgopYbxQSzSPFSlyCtE6YwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
AQsFAAOCAgEAyPxq4ZyBtKJEQtzmqdkI+28Yw4qDSBE0dj4QQfOErgK5hX29Bk4e
Auh6j0eyuRA8gtwngsE8fAAg84kcH/b+hM2zFW4MqpgjigA7oqA51VkIg+8Q9zdF
IHweV2kuSunZ2ANcGrr7o/Qy5y8D7URWUpUu8J1ZNyLg7YYMtpyjllTYhbfKpAgM
HUX1STCRfSPTgM/JxIsnll8RgacfhUmoOzOsrvvZ39h8cZZo96ksBRL4gvVQ++Hm
gzNTbYQXukR26Xfv112AEj5Xo3z3fsLP1KxZxd2p6/24XYktpZf2J71Np2CONdV4
gFgxFfPwvPyDO5pKice018qlXz0euhvK5g++s+TrSeZwleDTW4spP3TdVXNB96iZ
rrFkTT0SEBtd6iKqeFAX89BpshCUqOlsFdrf10i2dDiKsqxMod6a8i17RrtZL3q7
S0nqCsnyc1QvfKIQi08vfMZHHMbSS0Cg/5H8ISexM/R4SQc6C//IhEd5hUJ7E9AC
Sepr0xu1JBrglech4N+brHpGZK3Crofzu+hV+qruY5Wg21bD99zigxrgi3YaQPlA
6TJVsGk68h00QSy+Ri9dyuvyPnIDyMQfZLLIKCwsxznYXLtmIp+UoA3otGTijhmc
ZxFdEhd3cJrSPwihb/IvQJICFp3ya1aI7dLsaZO9h9kPc8GGLi7tiq8=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFozCCA4ugAwIBAgIUaRW0/q8ERGZUZqv+TVkC5lOwgX4wDQYJKoZIhvcNAQEL
BQAwYTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlVTMQswCQYDVQQHDAJVUzEPMA0G
A1UECgwGTXlIdXNoMQ0wCwYDVQQLDARIVVNIMRgwFgYDVQQDDA9saXRlLm15aHVz
aC5vcmcwHhcNMjAwMTAzMjAzMTAwWhcNMjEwMTAyMjAzMTAwWjBhMQswCQYDVQQG
EwJVUzELMAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMQ8wDQYDVQQKDAZNeUh1c2gx
DTALBgNVBAsMBEhVU0gxGDAWBgNVBAMMD2xpdGUubXlodXNoLm9yZzCCAiIwDQYJ
KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/oJh+MSxGOedvEthiqdfMEDfjzJ7AW
wwvuBtVULvnIvJq0WTMK8INjkMqnj+/ZE9a1aOOSpwN+9CuknhiySTdqHGl6EigM
/S19ZdN/7aC881VrMCncguHUOa8HLq5F4R0YCGis1Cor9vXf0GVFJ2mDfPB0C8iz
C+gMGnkDwuBy51fUcWbxZ5diRYh7YlOIUxpmb24On+X7sw+7nbmV12x8v644xRKJ
MazPIVrxIwXZ3tjz2NR7IMu+SrtCgMAW56M63NJ+iZBYDRoFVMRGEfAgGaFV2Hqi
Dx7q89sO2bhVg2lBBOW7403S+T8eK1rGFj7iGJoRLm/cgWwozZteXynHzicYEvX6
w1h0lu/OPQQk5AKRn+iI1kKLlT1auKIBXpfnpELnie3XgzzLyKt0pobVrWdMutlJ
83Zo7LmnhJYlcG7Qb0UczSyaIn+3dWo2HTbiyzJD23gmUbzFD4AVF1Ee4x5yT4Hb
Aw0FpQXDHX2MT04xleM2HdjE86ruZfNCegvQdRtgKRAiVNe4kP5ZzB7OeDX6pgeE
/e07tiHiFb2Im7J/IR4PmIh6SuYI/QObFXFXfvwCk/iJCps15/PryGfXZLdz+2z1
av1nwSglBRqeRX8HUBIgNcY0Lyq82BKfq4ZU3fKiIDuNV16OxCnFGZRw+ByRNrKR
KsgaJEi7qH6DAgMBAAGjUzBRMB0GA1UdDgQWBBRd+rxIkdKrpLsgz5/KiqZOYzUB
KDAfBgNVHSMEGDAWgBRd+rxIkdKrpLsgz5/KiqZOYzUBKDAPBgNVHRMBAf8EBTAD
AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBR9M0CnCvT1Zd5D/Wwy9ylH6CSFq6AEbdh
fMo8+NZl4J0FNji2Iv05xh+V3f+eyuf6oc8q0vsKeC5MZj/44AzqzxCSvMrUnh0V
GAiZQqLAVJIR/fi49bX9ku1yfQVJKzGFS93TcHMv9XYHJ4bSnQKlEOF5F6Wh9AO1
6GU/+vb0pSOfOv5+pUaj84AYCKQ8kp9nuNpOS12jyjc5hUogflPFAmeIpcIGjiso
Ln8+b+Xh0BZGNpdrvJ/wr8InxblMu5chtYrGAGo4mh4q3YWiwYJTkTIhoeTxF4eU
BOqMJa9lQgZE4T7V33jrIsuMPEZfACpj+gQNNzl8WQ+jzkZhBdYPTqhO9u1rlRXG
9VJfmuQ7+KLXAMFQgsHlX5Y5lux3CV36Knb5+1f/u0cdys1yb9mbQ/Ok3T8cuh6B
7Hs53JhAW47+CCnsNTaPzwti3wfzWhS2sjHz4IT1NcacsuDlxk7IykJ2U3auiufE
lRFpZoK81jsipEgRPBeF6OesXpldKxK9lnVJA/6ElApuo0amgg++fROQEpZSLyBM
lZdYrW6ZKnzCUZpNuwNHg1nfiit3RJ6hRLsk2jHrLDb5BVWzuJCNa7LK9bZ3IbUa
5jGb9Rplo+NgglQQYCflptksti9h5DN+GlVGxfJ9yzkg4/4ckmh75colcc1CTQAf
eER5vAF7og==
-----END CERTIFICATE-----

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,363 @@
// Copyright The Hush Developers 2019-2022
// Released under the GPLv3
use log::{info,error};
use std::sync::Arc;
use zcash_primitives::transaction::{TxId};
use crate::grpc_client::{ChainSpec, BlockId, BlockRange, RawTransaction, CompactBlock,
TransparentAddressBlockFilter, TxFilter, Empty, LightdInfo, Coinsupply};
use tonic::transport::{Channel, ClientTlsConfig};
use tokio_rustls::{rustls::ClientConfig};
use tonic::{Request};
use threadpool::ThreadPool;
use std::sync::mpsc::channel;
use crate::PubCertificate;
use crate::grpc_client::compact_tx_streamer_client::CompactTxStreamerClient;
mod danger {
use tokio_rustls::rustls;
use webpki;
pub struct NoCertificateVerification {}
impl rustls::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8]) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
Ok(rustls::ServerCertVerified::assertion())
}
}
}
async fn get_client(uri: &http::Uri, no_cert: bool) -> Result<CompactTxStreamerClient<Channel>, Box<dyn std::error::Error>> {
let channel = if uri.scheme_str() == Some("http") {
Channel::builder(uri.clone()).connect().await?
} else {
let mut config = ClientConfig::new();
config.alpn_protocols.push(b"h2".to_vec());
config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
config.root_store.add_pem_file(
&mut PubCertificate::get("lightwalletd-lite.myhush.pem").unwrap().as_ref()).unwrap();
if no_cert {
config.dangerous()
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification {}));
}
let tls = ClientTlsConfig::new()
.rustls_client_config(config)
.domain_name(uri.host().unwrap());
Channel::builder(uri.clone())
.tls_config(tls)
.connect()
.await?
};
Ok(CompactTxStreamerClient::new(channel))
}
// ==============
// GRPC code
// ==============
async fn get_lightd_info(uri: &http::Uri, no_cert: bool) -> Result<LightdInfo, Box<dyn std::error::Error>> {
let mut client = get_client(uri, no_cert).await?;
let request = Request::new(Empty {});
let response = client.get_lightd_info(request).await?;
Ok(response.into_inner())
}
pub fn get_info(uri: &http::Uri, no_cert: bool) -> Result<LightdInfo, String> {
let mut rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
rt.block_on(get_lightd_info(uri, no_cert)).map_err( |e| e.to_string())
}
async fn get_coinsupply_info(uri: &http::Uri, no_cert: bool) -> Result<Coinsupply, Box<dyn std::error::Error>> {
let mut client = get_client(uri, no_cert).await?;
let request = Request::new(Empty {});
let response = client.get_coinsupply(request).await?;
Ok(response.into_inner())
}
pub fn get_coinsupply(uri: http::Uri, no_cert: bool) -> Result<Coinsupply, String> {
let mut rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
rt.block_on(get_coinsupply_info(&uri, no_cert)).map_err( |e| e.to_string())
}
async fn get_block_range<F : 'static + std::marker::Send>(
uri: &http::Uri,
start_height: u64,
end_height: u64,
no_cert: bool,
pool: ThreadPool,
c: F
) -> Result<(), Box<dyn std::error::Error>>
where F : Fn(&[u8], u64) {
let mut client = get_client(uri, no_cert).await?;
let bs = BlockId { height: start_height, hash: vec![] };
let be = BlockId { height: end_height, hash: vec![] };
let request = Request::new(BlockRange { start: Some(bs), end: Some(be) });
let (tx, rx) = channel::<Option<CompactBlock>>();
let (ftx, frx) = channel();
pool.execute(move || {
while let Ok(Some(block)) = rx.recv() {
use prost::Message;
let mut encoded_buf = vec![];
if let Err(e) = block.encode(&mut encoded_buf) {
error!("Error encoding block: {:?}", e);
break;
}
c(&encoded_buf, block.height);
}
if let Err(e) = ftx.send(Ok(())) {
error!("Error sending completion signal: {:?}", e);
}
});
let mut response = client.get_block_range(request).await?.into_inner();
while let Some(block) = response.message().await? {
if let Err(e) = tx.send(Some(block)) {
error!("Error sending block to channel: {:?}", e);
break;
}
}
if let Err(e) = tx.send(None) {
error!("Error sending end signal to channel: {:?}", e);
}
frx.iter().take(1).collect::<Result<Vec<()>, String>>()?;
Ok(())
}
pub fn fetch_blocks<F : 'static + std::marker::Send>(uri: &http::Uri, start_height: u64, end_height: u64, no_cert: bool, pool: ThreadPool, c: F) -> Result<(), String>
where F : Fn(&[u8], u64) {
let mut rt = tokio::runtime::Runtime::new().map_err(|e| format!("Error creating runtime {:?}", e))?;
fetch_blocks_with_runtime(&mut rt, uri, start_height, end_height, no_cert, pool, c)
}
pub fn fetch_blocks_with_runtime<F : 'static + std::marker::Send>(rt: &mut tokio::runtime::Runtime, uri: &http::Uri, start_height: u64, end_height: u64, no_cert: bool, pool: ThreadPool, c: F) -> Result<(), String>
where F : Fn(&[u8], u64) {
match rt.block_on(get_block_range(uri, start_height, end_height, no_cert, pool, c)) {
Ok(o) => Ok(o),
Err(e) => {
let e = format!("Error fetching blocks {:?}", e);
error!("{}", e);
Err(e)
}
}
}
// get_address_txids GRPC call
async fn get_address_txids<F : 'static + std::marker::Send>(
uri: &http::Uri,
address: String,
start_height: u64,
end_height: u64,
no_cert: bool,
c: F
) -> Result<(), Box<dyn std::error::Error>>
where F : Fn(&[u8], u64) {
let mut client = match get_client(uri, no_cert).await {
Ok(client) => client,
Err(e) => {
error!("Error creating client: {:?}", e);
return Err(e.into());
}
};
let start = Some(BlockId{ height: start_height, hash: vec!()});
let end = Some(BlockId{ height: end_height, hash: vec!()});
let request = Request::new(TransparentAddressBlockFilter{ address, range: Some(BlockRange{ start, end }) });
let maybe_response = match client.get_address_txids(request).await {
Ok(response) => response,
Err(e) => {
error!("Error getting address txids: {:?}", e);
return Err(e.into());
}
};
let mut response = maybe_response.into_inner();
while let Some(tx) = response.message().await? {
c(&tx.data, tx.height);
}
Ok(())
}
// function to monitor mempool transactions
pub async fn monitor_mempool<F: 'static + std::marker::Send>(
uri: &http::Uri,
no_cert: bool,
mut c: F
) -> Result<(), Box<dyn std::error::Error>>
where
F: FnMut(RawTransaction) -> Result<(), Box<dyn std::error::Error>>,
{
let mut client = get_client(uri, no_cert)
.await
.map_err(|e| format!("Error getting client: {:?}", e))?;
let request = Request::new(Empty {});
let mut response = client
.get_mempool_stream(request)
.await
.map_err(|e| format!("{}", e))?
.into_inner();
while let Ok(Some(rtx)) = response.message().await {
if let Err(e) = c(rtx) {
info!("Error processing RawTransaction: {:?}", e);
}
}
Ok(())
}
pub fn fetch_transparent_txids<F : 'static + std::marker::Send>(
uri: &http::Uri,
address: String,
start_height: u64,
end_height: u64,
no_cert: bool,
c: F
) -> Result<(), String>
where F : Fn(&[u8], u64) {
let mut rt = match tokio::runtime::Runtime::new() {
Ok(r) => r,
Err(e) => {
let e = format!("Error creating runtime {:?}", e);
error!("{}", e);
return Err(e);
}
};
match rt.block_on(get_address_txids(uri, address.clone(), start_height, end_height, no_cert, c)) {
Ok(o) => Ok(o),
Err(e) => {
let e = format!("Error with get_address_txids runtime {:?}", e);
error!("{}", e);
return Err(e)
}
}
}
// get_transaction GRPC call
async fn get_transaction(uri: &http::Uri, txid: TxId, no_cert: bool)
-> Result<RawTransaction, Box<dyn std::error::Error>> {
let mut client = get_client(uri, no_cert).await?;
let request = Request::new(TxFilter { block: None, index: 0, hash: txid.0.to_vec() });
let response = client.get_transaction(request).await?;
Ok(response.into_inner())
}
pub fn fetch_full_tx(uri: &http::Uri, txid: TxId, no_cert: bool) -> Result<Vec<u8>, String> {
let mut rt = match tokio::runtime::Runtime::new() {
Ok(r) => r,
Err(e) => {
let errstr = format!("Error creating runtime {}", e.to_string());
error!("{}", errstr);
return Err(errstr);
}
};
match rt.block_on(get_transaction(uri, txid, no_cert)) {
Ok(rawtx) => Ok(rawtx.data.to_vec()),
Err(e) => {
let errstr = format!("Error in get_transaction runtime {}", e.to_string());
error!("{}", errstr);
Err(errstr)
}
}
}
// send_transaction GRPC call
async fn send_transaction(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> Result<String, Box<dyn std::error::Error>> {
let mut client = get_client(uri, no_cert).await?;
let request = Request::new(RawTransaction {data: tx_bytes.to_vec(), height: 0});
let response = client.send_transaction(request).await?;
let sendresponse = response.into_inner();
if sendresponse.error_code == 0 {
let mut txid = sendresponse.error_message;
if txid.starts_with("\"") && txid.ends_with("\"") {
txid = txid[1..txid.len()-1].to_string();
}
Ok(txid)
} else {
Err(Box::from(format!("Error: {:?}", sendresponse)))
}
}
pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> Result<String, String> {
let mut rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
rt.block_on(send_transaction(uri, no_cert, tx_bytes)).map_err( |e| e.to_string())
}
// get_latest_block GRPC call
async fn get_latest_block(uri: &http::Uri, no_cert: bool) -> Result<BlockId, Box<dyn std::error::Error>> {
let mut client = get_client(uri, no_cert).await?;
let request = Request::new(ChainSpec {});
let response = client.get_latest_block(request).await?;
Ok(response.into_inner())
}
pub fn fetch_latest_block(uri: &http::Uri, no_cert: bool) -> Result<BlockId, String> {
let mut rt = match tokio::runtime::Runtime::new() {
Ok(r) => r,
Err(e) => {
let errstr = format!("Error creating runtime {}", e.to_string());
error!("{}", errstr);
return Err(errstr);
}
};
rt.block_on(get_latest_block(uri, no_cert)).map_err(|e| {
let errstr = format!("Error getting latest block {}", e.to_string());
error!("{}", errstr);
errstr
})
}

View File

@@ -0,0 +1,29 @@
// Copyright The Hush Developers 2019-2022
// Released under the GPLv3
#[macro_use]
extern crate rust_embed;
pub mod lightclient;
pub mod grpcconnector;
pub mod lightwallet;
pub mod commands;
#[cfg(feature = "embed_params")]
#[derive(RustEmbed)]
#[folder = "zcash-params/"]
pub struct SaplingParams;
#[derive(RustEmbed)]
#[folder = "res/"]
pub struct PubCertificate;
// Anchor depth back from the tip for shielded spends. MUST be > 0: with 0 the wallet anchors to
// the absolute chain tip, which races the node committing that anchor and intermittently triggers
// "bad-txns-shielded-requirements-not-met" (missing sapling anchor) on broadcast. 4 matches upstream
// zecwallet-lite and uses a confirmed, reorg-stable anchor.
pub const ANCHOR_OFFSET: u32 = 4;
pub mod grpc_client {
tonic::include_proto!("cash.z.wallet.sdk.rpc");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
//! Structs for handling supported address types.
use pairing::bls12_381::Bls12;
use zcash_primitives::primitives::PaymentAddress;
use zcash_client_backend::encoding::{decode_payment_address, decode_transparent_address};
use zcash_primitives::legacy::TransparentAddress;
/// An address that funds can be sent to.
pub enum RecipientAddress {
Shielded(PaymentAddress<Bls12>),
Transparent(TransparentAddress),
}
impl From<PaymentAddress<Bls12>> for RecipientAddress {
fn from(addr: PaymentAddress<Bls12>) -> Self {
RecipientAddress::Shielded(addr)
}
}
impl From<TransparentAddress> for RecipientAddress {
fn from(addr: TransparentAddress) -> Self {
RecipientAddress::Transparent(addr)
}
}
impl RecipientAddress {
pub fn from_str(s: &str, hrp_sapling_address: &str, b58_pubkey_address: [u8; 1], b58_script_address: [u8; 1]) -> Option<Self> {
// Try to match a sapling z address
if let Some(pa) = match decode_payment_address(hrp_sapling_address, s) {
Ok(ret) => ret,
Err(_) => None
}
{
Some(RecipientAddress::Shielded(pa)) // Matched a shielded address
} else if let Some(addr) = match decode_transparent_address(
&b58_pubkey_address, &b58_script_address, s) {
Ok(ret) => ret,
Err(_) => None
}
{
Some(RecipientAddress::Transparent(addr)) // Matched a transparent address
} else {
None // Didn't match anything
}
}
}

View File

@@ -0,0 +1,558 @@
use std::io::{self, Read, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use pairing::bls12_381::{Bls12};
use ff::{PrimeField, PrimeFieldRepr};
use zcash_primitives::{
block::BlockHash,
merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::Node,
serialize::{Vector, Optional},
transaction::{
components::{OutPoint},
TxId,
},
note_encryption::{Memo,},
zip32::{ExtendedFullViewingKey,},
JUBJUB,
primitives::{Diversifier, Note,},
jubjub::{
JubjubEngine,
fs::{Fs, FsRepr},
}
};
use zcash_primitives::zip32::ExtendedSpendingKey;
pub struct BlockData {
pub height: i32,
pub hash: BlockHash,
pub tree: CommitmentTree<Node>,
}
impl BlockData {
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let height = reader.read_i32::<LittleEndian>()?;
let mut hash_bytes = [0; 32];
reader.read_exact(&mut hash_bytes)?;
let tree = CommitmentTree::<Node>::read(&mut reader)?;
let endtag = reader.read_u64::<LittleEndian>()?;
if endtag != 11 {
println!("End tag for blockdata {}", endtag);
}
Ok(BlockData{
height,
hash: BlockHash{ 0: hash_bytes },
tree
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_i32::<LittleEndian>(self.height)?;
writer.write_all(&self.hash.0)?;
self.tree.write(&mut writer)?;
writer.write_u64::<LittleEndian>(11)
}
}
pub struct SaplingNoteData {
pub(super) account: usize,
pub(super) extfvk: ExtendedFullViewingKey, // Technically, this should be recoverable from the account number, but we're going to refactor this in the future, so I'll write it again here.
pub diversifier: Diversifier,
pub note: Note<Bls12>,
pub(super) witnesses: Vec<IncrementalWitness<Node>>,
pub(super) nullifier: [u8; 32],
pub spent: Option<TxId>, // If this note was confirmed spent
pub unconfirmed_spent: Option<TxId>, // If this note was spent in a send, but has not yet been confirmed.
pub memo: Option<Memo>,
pub is_change: bool,
// TODO: We need to remove the unconfirmed_spent (i.e., set it to None) if the Tx has expired
}
/// Reads an FsRepr from [u8] of length 32
/// This will panic (abort) if length provided is
/// not correct
/// TODO: This is duplicate from rustzcash.rs
fn read_fs(from: &[u8]) -> FsRepr {
assert_eq!(from.len(), 32);
let mut f = <<Bls12 as JubjubEngine>::Fs as PrimeField>::Repr::default();
f.read_le(from).expect("length is 32 bytes");
f
}
// Reading a note also needs the corresponding address to read from.
pub fn read_note<R: Read>(mut reader: R) -> io::Result<(u64, Fs)> {
let value = reader.read_u64::<LittleEndian>()?;
let mut r_bytes: [u8; 32] = [0; 32];
reader.read_exact(&mut r_bytes)?;
let r = match Fs::from_repr(read_fs(&r_bytes)) {
Ok(r) => r,
Err(_) => return Err(io::Error::new(
io::ErrorKind::InvalidInput, "Couldn't parse randomness"))
};
Ok((value, r))
}
impl SaplingNoteData {
fn serialized_version() -> u64 {
1
}
pub fn new(
extfvk: &ExtendedFullViewingKey,
output: zcash_client_backend::wallet::WalletShieldedOutput
) -> Self {
let witness = output.witness;
let nf = {
let mut nf = [0; 32];
nf.copy_from_slice(
&output
.note
.nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB),
);
nf
};
SaplingNoteData {
account: output.account,
extfvk: extfvk.clone(),
diversifier: output.to.diversifier,
note: output.note,
witnesses: vec![witness],
nullifier: nf,
spent: None,
unconfirmed_spent: None,
memo: None,
is_change: output.is_change,
}
}
// Reading a note also needs the corresponding address to read from.
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let _version = reader.read_u64::<LittleEndian>()?;
let account = reader.read_u64::<LittleEndian>()? as usize;
let extfvk = ExtendedFullViewingKey::read(&mut reader)?;
let mut diversifier_bytes = [0u8; 11];
reader.read_exact(&mut diversifier_bytes)?;
let diversifier = Diversifier{0: diversifier_bytes};
// To recover the note, read the value and r, and then use the payment address
// to recreate the note
let (value, r) = read_note(&mut reader)?; // TODO: This method is in a different package, because of some fields that are private
let maybe_note = extfvk.fvk.vk.into_payment_address(diversifier, &JUBJUB).unwrap().create_note(value, r, &JUBJUB);
let note = match maybe_note {
Some(n) => Ok(n),
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Couldn't create the note for the address"))
}?;
let witnesses = Vector::read(&mut reader, |r| IncrementalWitness::<Node>::read(r))?;
let mut nullifier = [0u8; 32];
reader.read_exact(&mut nullifier)?;
// Note that this is only the spent field, we ignore the unconfirmed_spent field.
// The reason is that unconfirmed spents are only in memory, and we need to get the actual value of spent
// from the blockchain anyway.
let spent = Optional::read(&mut reader, |r| {
let mut txid_bytes = [0u8; 32];
r.read_exact(&mut txid_bytes)?;
Ok(TxId{0: txid_bytes})
})?;
let memo = Optional::read(&mut reader, |r| {
let mut memo_bytes = [0u8; 512];
r.read_exact(&mut memo_bytes)?;
match Memo::from_bytes(&memo_bytes) {
Some(m) => Ok(m),
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Couldn't create the memo"))
}
})?;
let is_change: bool = reader.read_u8()? > 0;
Ok(SaplingNoteData {
account,
extfvk,
diversifier,
note,
witnesses,
nullifier,
spent,
unconfirmed_spent: None,
memo,
is_change,
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
// Write a version number first, so we can later upgrade this if needed.
writer.write_u64::<LittleEndian>(SaplingNoteData::serialized_version())?;
writer.write_u64::<LittleEndian>(self.account as u64)?;
self.extfvk.write(&mut writer)?;
writer.write_all(&self.diversifier.0)?;
// Writing the note means writing the note.value and note.r. The Note is recoverable
// from these 2 values and the Payment address.
writer.write_u64::<LittleEndian>(self.note.value)?;
let mut rcm = [0; 32];
self.note.r.into_repr().write_le(&mut rcm[..])?;
writer.write_all(&rcm)?;
Vector::write(&mut writer, &self.witnesses, |wr, wi| wi.write(wr) )?;
writer.write_all(&self.nullifier)?;
Optional::write(&mut writer, &self.spent, |w, t| w.write_all(&t.0))?;
Optional::write(&mut writer, &self.memo, |w, m| w.write_all(m.as_bytes()))?;
writer.write_u8(if self.is_change {1} else {0})?;
// Note that we don't write the unconfirmed_spent field, because if the wallet is restarted,
// we don't want to be beholden to any expired txns
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct Utxo {
pub address: String,
pub txid: TxId,
pub output_index: u64,
pub script: Vec<u8>,
pub value: u64,
pub height: i32,
pub spent: Option<TxId>, // If this utxo was confirmed spent
pub unconfirmed_spent: Option<TxId>, // If this utxo was spent in a send, but has not yet been confirmed.
}
impl Utxo {
pub fn serialized_version() -> u64 {
return 1;
}
pub fn to_outpoint(&self) -> OutPoint {
OutPoint { hash: self.txid.0, n: self.output_index as u32 }
}
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let version = reader.read_u64::<LittleEndian>()?;
assert_eq!(version, Utxo::serialized_version());
let address_len = reader.read_i32::<LittleEndian>()?;
let mut address_bytes = vec![0; address_len as usize];
reader.read_exact(&mut address_bytes)?;
let address = String::from_utf8(address_bytes).unwrap();
assert_eq!(address.chars().take(1).collect::<Vec<char>>()[0], 'R');
let mut txid_bytes = [0; 32];
reader.read_exact(&mut txid_bytes)?;
let txid = TxId { 0: txid_bytes };
let output_index = reader.read_u64::<LittleEndian>()?;
let value = reader.read_u64::<LittleEndian>()?;
let height = reader.read_i32::<LittleEndian>()?;
let script = Vector::read(&mut reader, |r| {
let mut byte = [0; 1];
r.read_exact(&mut byte)?;
Ok(byte[0])
})?;
let spent = Optional::read(&mut reader, |r| {
let mut txbytes = [0u8; 32];
r.read_exact(&mut txbytes)?;
Ok(TxId{0: txbytes})
})?;
// Note that we don't write the unconfirmed spent field, because if the wallet is restarted, we'll reset any unconfirmed stuff.
Ok(Utxo {
address,
txid,
output_index,
script,
value,
height,
spent,
unconfirmed_spent: None::<TxId>,
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_u64::<LittleEndian>(Utxo::serialized_version())?;
writer.write_u32::<LittleEndian>(self.address.as_bytes().len() as u32)?;
writer.write_all(self.address.as_bytes())?;
writer.write_all(&self.txid.0)?;
writer.write_u64::<LittleEndian>(self.output_index)?;
writer.write_u64::<LittleEndian>(self.value)?;
writer.write_i32::<LittleEndian>(self.height)?;
Vector::write(&mut writer, &self.script, |w, b| w.write_all(&[*b]))?;
Optional::write(&mut writer, &self.spent, |w, txid| w.write_all(&txid.0))?;
// Note that we don't write the unconfirmed spent field, because if the wallet is restarted, we'll reset any unconfirmed stuff.
Ok(())
}
}
pub struct OutgoingTxMetadata {
pub address: String,
pub value : u64,
pub memo : Memo,
}
impl OutgoingTxMetadata {
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let address_len = reader.read_u64::<LittleEndian>()?;
let mut address_bytes = vec![0; address_len as usize];
reader.read_exact(&mut address_bytes)?;
let address = String::from_utf8(address_bytes).unwrap();
let value = reader.read_u64::<LittleEndian>()?;
let mut memo_bytes = [0u8; 512];
reader.read_exact(&mut memo_bytes)?;
let memo = Memo::from_bytes(&memo_bytes).unwrap();
Ok(OutgoingTxMetadata{
address,
value,
memo,
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
// Strings are written as len + utf8
writer.write_u64::<LittleEndian>(self.address.as_bytes().len() as u64)?;
writer.write_all(self.address.as_bytes())?;
writer.write_u64::<LittleEndian>(self.value)?;
writer.write_all(self.memo.as_bytes())
}
}
#[derive(Debug)]
pub struct IncomingTxMetadata {
pub address: String,
pub value : u64,
pub memo : Memo,
pub incoming_mempool: bool,
}
impl IncomingTxMetadata {
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let address_len = reader.read_u64::<LittleEndian>()?;
let mut address_bytes = vec![0; address_len as usize];
reader.read_exact(&mut address_bytes)?;
let address = String::from_utf8(address_bytes).unwrap();
let value = reader.read_u64::<LittleEndian>()?;
let incoming_mempool = true;
// let position = 0;
let mut memo_bytes = [0u8; 512];
reader.read_exact(&mut memo_bytes)?;
let memo = Memo::from_bytes(&memo_bytes).unwrap();
Ok(IncomingTxMetadata{
address,
value,
memo,
incoming_mempool,
// position,
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
// Strings are written as len + utf8
writer.write_u64::<LittleEndian>(self.address.as_bytes().len() as u64)?;
writer.write_all(self.address.as_bytes())?;
writer.write_u64::<LittleEndian>(self.value)?;
writer.write_all(self.memo.as_bytes())
}
}
pub struct WalletTx {
// Block in which this tx was included
pub block: i32,
// Timestamp of Tx. Added in v4
pub datetime: u64,
// Txid of this transaction. It's duplicated here (It is also the Key in the HashMap that points to this
// WalletTx in LightWallet::txs)
pub txid: TxId,
// List of all notes received in this tx. Some of these might be change notes.
pub notes: Vec<SaplingNoteData>,
// List of all Utxos received in this Tx. Some of these might be change notes
pub utxos: Vec<Utxo>,
// Total shielded value spent in this Tx. Note that this is the value of the wallet's notes spent.
// Some change may be returned in one of the notes above. Subtract the two to get the actual value spent.
// Also note that even after subtraction, you might need to account for transparent inputs and outputs
// to make sure the value is accurate.
pub total_shielded_value_spent: u64,
// Total amount of transparent funds that belong to us that were spent in this Tx.
pub total_transparent_value_spent : u64,
// All outgoing sapling sends to addresses outside this wallet
pub outgoing_metadata: Vec<OutgoingTxMetadata>,
pub incoming_metadata: Vec<IncomingTxMetadata>,
// Whether this TxID was downloaded from the server and scanned for Memos
pub full_tx_scanned: bool,
}
impl WalletTx {
pub fn serialized_version() -> u64 {
return 5;
}
pub fn new(height: i32, datetime: u64, txid: &TxId) -> Self {
WalletTx {
block: height,
datetime,
txid: txid.clone(),
notes: vec![],
utxos: vec![],
total_shielded_value_spent: 0,
total_transparent_value_spent: 0,
outgoing_metadata: vec![],
incoming_metadata: vec![],
full_tx_scanned: false,
}
}
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let version = reader.read_u64::<LittleEndian>()?;
assert!(version <= WalletTx::serialized_version(), "Version mismatch. Please restore with your Seed");
let block = reader.read_i32::<LittleEndian>()?;
let datetime = if version >= 4 {
reader.read_u64::<LittleEndian>()?
} else {
0
};
let mut txid_bytes = [0u8; 32];
reader.read_exact(&mut txid_bytes)?;
let txid = TxId{0: txid_bytes};
let notes = Vector::read(&mut reader, |r| SaplingNoteData::read(r))?;
let utxos = Vector::read(&mut reader, |r| Utxo::read(r))?;
let total_shielded_value_spent = reader.read_u64::<LittleEndian>()?;
let total_transparent_value_spent = reader.read_u64::<LittleEndian>()?;
let outgoing_metadata = Vector::read(&mut reader, |r| OutgoingTxMetadata::read(r))?;
// Read incoming_metadata only if version is 5 or higher
let incoming_metadata = if version >= 5 {
Vector::read(&mut reader, |r| IncomingTxMetadata::read(r))?
} else {
vec![]
};
let full_tx_scanned = reader.read_u8()? > 0;
Ok(WalletTx {
block,
datetime,
txid,
notes,
utxos,
total_shielded_value_spent,
total_transparent_value_spent,
outgoing_metadata,
incoming_metadata,
full_tx_scanned
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_u64::<LittleEndian>(WalletTx::serialized_version())?;
writer.write_i32::<LittleEndian>(self.block)?;
writer.write_u64::<LittleEndian>(self.datetime)?;
writer.write_all(&self.txid.0)?;
Vector::write(&mut writer, &self.notes, |w, nd| nd.write(w))?;
Vector::write(&mut writer, &self.utxos, |w, u| u.write(w))?;
writer.write_u64::<LittleEndian>(self.total_shielded_value_spent)?;
writer.write_u64::<LittleEndian>(self.total_transparent_value_spent)?;
// Write the outgoing metadata
Vector::write(&mut writer, &self.outgoing_metadata, |w, om| om.write(w))?;
Vector::write(&mut writer, &self.incoming_metadata, |w, om| om.write(w))?;
writer.write_u8(if self.full_tx_scanned {1} else {0})?;
Ok(())
}
}
pub struct SpendableNote {
pub txid: TxId,
pub nullifier: [u8; 32],
pub diversifier: Diversifier,
pub note: Note<Bls12>,
pub witness: IncrementalWitness<Node>,
pub extsk: ExtendedSpendingKey,
}
impl SpendableNote {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &Option<ExtendedSpendingKey>) -> Option<Self> {
// Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet.
if nd.spent.is_none() && nd.unconfirmed_spent.is_none() && extsk.is_some() &&
nd.witnesses.len() >= (anchor_offset + 1) {
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1);
witness.map(|w| SpendableNote {
txid,
nullifier: nd.nullifier,
diversifier: nd.diversifier,
note: nd.note.clone(),
witness: w.clone(),
extsk: extsk.clone().unwrap(),
})
} else {
None
}
}
}

View File

@@ -0,0 +1,126 @@
use ring::{
hmac::{self, Context, Key},
};
use lazy_static::lazy_static;
use secp256k1::{PublicKey, Secp256k1, SecretKey, SignOnly, VerifyOnly, Error};
lazy_static! {
static ref SECP256K1_SIGN_ONLY: Secp256k1<SignOnly> = Secp256k1::signing_only();
static ref SECP256K1_VERIFY_ONLY: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
}
/// Random entropy, part of extended key.
type ChainCode = Vec<u8>;
const HARDENED_KEY_START_INDEX: u32 = 2_147_483_648; // 2 ** 31
/// KeyIndex indicates the key type and index of a child key.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum KeyIndex {
/// Normal key, index range is from 0 to 2 ** 31 - 1
Normal(u32),
/// Hardened key, index range is from 2 ** 31 to 2 ** 32 - 1
Hardened(u32),
}
impl KeyIndex {
/// Check index range.
pub fn is_valid(self) -> bool {
match self {
KeyIndex::Normal(i) => i < HARDENED_KEY_START_INDEX,
KeyIndex::Hardened(i) => i >= HARDENED_KEY_START_INDEX,
}
}
/// Generate Hardened KeyIndex from normalize index value.
pub fn hardened_from_normalize_index(i: u32) -> Result<KeyIndex, Error> {
if i < HARDENED_KEY_START_INDEX {
Ok(KeyIndex::Hardened(HARDENED_KEY_START_INDEX + i))
} else {
Ok(KeyIndex::Hardened(i))
}
}
/// Generate KeyIndex from raw index value.
pub fn from_index(i: u32) -> Result<Self, Error> {
if i < HARDENED_KEY_START_INDEX {
Ok(KeyIndex::Normal(i))
} else {
Ok(KeyIndex::Hardened(i))
}
}
}
impl From<u32> for KeyIndex {
fn from(index: u32) -> Self {
KeyIndex::from_index(index).expect("KeyIndex")
}
}
/// ExtendedPrivKey is used for child key derivation.
/// See [secp256k1 crate documentation](https://docs.rs/secp256k1) for SecretKey signatures usage.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtendedPrivKey {
pub private_key: SecretKey,
pub chain_code: ChainCode,
}
impl ExtendedPrivKey {
/// Generate an ExtendedPrivKey from seed
pub fn with_seed(seed: &[u8]) -> Result<ExtendedPrivKey, Error> {
let signature = {
let signing_key = Key::new(hmac::HMAC_SHA512, b"Bitcoin seed");
let mut h = Context::with_key(&signing_key);
h.update(&seed);
h.sign()
};
let sig_bytes = signature.as_ref();
let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2);
let private_key = SecretKey::from_slice(key)?;
Ok(ExtendedPrivKey {
private_key,
chain_code: chain_code.to_vec(),
})
}
fn sign_hardended_key(&self, index: u32) -> ring::hmac::Tag {
let signing_key = Key::new(hmac::HMAC_SHA512, &self.chain_code);
let mut h = Context::with_key(&signing_key);
h.update(&[0x00]);
h.update(&self.private_key[..]);
h.update(&index.to_be_bytes());
h.sign()
}
fn sign_normal_key(&self, index: u32) -> ring::hmac::Tag {
let signing_key = Key::new(hmac::HMAC_SHA512, &self.chain_code);
let mut h = Context::with_key(&signing_key);
let public_key = PublicKey::from_secret_key(&SECP256K1_SIGN_ONLY, &self.private_key);
h.update(&public_key.serialize());
h.update(&index.to_be_bytes());
h.sign()
}
/// Derive a child key from ExtendedPrivKey.
pub fn derive_private_key(&self, key_index: KeyIndex) -> Result<ExtendedPrivKey, Error> {
if !key_index.is_valid() {
return Err(Error::InvalidTweak);
}
let signature = match key_index {
KeyIndex::Hardened(index) => self.sign_hardended_key(index),
KeyIndex::Normal(index) => self.sign_normal_key(index),
};
let sig_bytes = signature.as_ref();
let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2);
let mut private_key = SecretKey::from_slice(key)?;
private_key.add_assign(&self.private_key[..])?;
Ok(ExtendedPrivKey {
private_key,
chain_code: chain_code.to_vec(),
})
}
}

View File

@@ -0,0 +1,123 @@
//! Abstractions over the proving system and parameters for ease of use.
use bellman::groth16::{prepare_verifying_key, Parameters, PreparedVerifyingKey};
use pairing::bls12_381::{Bls12, Fr};
use zcash_primitives::{
jubjub::{edwards, fs::Fs, Unknown},
primitives::{Diversifier, PaymentAddress, ProofGenerationKey},
redjubjub::{PublicKey, Signature},
transaction::components::Amount
};
use zcash_primitives::{
merkle_tree::CommitmentTreeWitness, prover::TxProver, sapling::Node,
transaction::components::GROTH_PROOF_SIZE, JUBJUB,
};
use zcash_proofs::sapling::SaplingProvingContext;
/// An implementation of [`TxProver`] using Sapling Spend and Output parameters provided
/// in-memory.
pub struct InMemTxProver {
spend_params: Parameters<Bls12>,
spend_vk: PreparedVerifyingKey<Bls12>,
output_params: Parameters<Bls12>,
}
impl InMemTxProver {
pub fn new(spend_params: &[u8], output_params: &[u8]) -> Self {
// Deserialize params
let spend_params = Parameters::<Bls12>::read(spend_params, false)
.expect("couldn't deserialize Sapling spend parameters file");
let output_params = Parameters::<Bls12>::read(output_params, false)
.expect("couldn't deserialize Sapling spend parameters file");
// Prepare verifying keys
let spend_vk = prepare_verifying_key(&spend_params.vk);
InMemTxProver {
spend_params,
spend_vk,
output_params,
}
}
}
impl TxProver for InMemTxProver {
type SaplingProvingContext = SaplingProvingContext;
fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext {
SaplingProvingContext::new()
}
fn spend_proof(
&self,
ctx: &mut Self::SaplingProvingContext,
proof_generation_key: ProofGenerationKey<Bls12>,
diversifier: Diversifier,
rcm: Fs,
ar: Fs,
value: u64,
anchor: Fr,
witness: CommitmentTreeWitness<Node>,
) -> Result<
(
[u8; GROTH_PROOF_SIZE],
edwards::Point<Bls12, Unknown>,
PublicKey<Bls12>,
),
(),
> {
let (proof, cv, rk) = ctx.spend_proof(
proof_generation_key,
diversifier,
rcm,
ar,
value,
anchor,
witness,
&self.spend_params,
&self.spend_vk,
&JUBJUB,
)?;
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
proof
.write(&mut zkproof[..])
.expect("should be able to serialize a proof");
Ok((zkproof, cv, rk))
}
fn output_proof(
&self,
ctx: &mut Self::SaplingProvingContext,
esk: Fs,
payment_address: PaymentAddress<Bls12>,
rcm: Fs,
value: u64,
) -> ([u8; GROTH_PROOF_SIZE], edwards::Point<Bls12, Unknown>) {
let (proof, cv) = ctx.output_proof(
esk,
payment_address,
rcm,
value,
&self.output_params,
&JUBJUB,
);
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
proof
.write(&mut zkproof[..])
.expect("should be able to serialize a proof");
(zkproof, cv)
}
fn binding_sig(
&self,
ctx: &mut Self::SaplingProvingContext,
value_balance: Amount,
sighash: &[u8; 32],
) -> Result<Signature, ()> {
ctx.binding_sig(value_balance, sighash, &JUBJUB)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
use std::io::{self, Read, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
pub fn read_string<R: Read>(mut reader: R) -> io::Result<String> {
// Strings are written as <littleendian> len + bytes
let str_len = reader.read_u64::<LittleEndian>()?;
let mut str_bytes = vec![0; str_len as usize];
reader.read_exact(&mut str_bytes)?;
let str = String::from_utf8(str_bytes).map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, e.to_string())
})?;
Ok(str)
}
pub fn write_string<W: Write>(mut writer: W, s: &String) -> io::Result<()> {
// Strings are written as len + utf8
writer.write_u64::<LittleEndian>(s.as_bytes().len() as u64)?;
writer.write_all(s.as_bytes())
}

View File

@@ -0,0 +1,585 @@
use std::io::{self, Read, Write};
use std::io::{Error, ErrorKind};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use pairing::bls12_381::{Bls12};
use sodiumoxide::crypto::secretbox;
use zcash_primitives::{
serialize::{Vector, Optional},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
primitives::{PaymentAddress},
};
use crate::lightclient::{LightClientConfig};
use crate::lightwallet::{LightWallet, utils};
#[derive(PartialEq, Debug, Clone)]
pub enum WalletTKeyType {
HdKey = 0,
ImportedKey = 1,
}
// A struct that holds z-address private keys or view keys
#[derive(Clone, Debug, PartialEq)]
pub struct WalletTKey {
pub(super) keytype: WalletTKeyType,
locked: bool,
pub(super) address: String,
pub(super) tkey: Option<secp256k1::SecretKey>,
// If locked, the encrypted key is here
enc_key: Option<Vec<u8>>,
nonce: Option<Vec<u8>>,
}
impl WalletTKey {
pub fn new_hdkey(key: secp256k1::SecretKey, address: String) -> Self {
WalletTKey {
keytype: WalletTKeyType::HdKey,
locked: false,
address,
tkey: Some(key),
enc_key: None,
nonce: None,
}
}
pub fn import_hdkey(key: secp256k1::SecretKey, address: String) -> Self {
WalletTKey {
keytype: WalletTKeyType::ImportedKey,
locked: false,
address,
tkey: Some(key),
enc_key: None,
nonce: None,
}
}
fn serialized_version() -> u8 {
return 1;
}
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
let version = inp.read_u8()?;
assert!(version <= Self::serialized_version());
let keytype: WalletTKeyType = match inp.read_u32::<LittleEndian>()? {
0 => Ok(WalletTKeyType::HdKey),
1 => Ok(WalletTKeyType::ImportedKey),
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown tkey type {}", n)))
}?;
let locked = inp.read_u8()? > 0;
let address = utils::read_string(&mut inp)?;
let tkey = Optional::read(&mut inp, |r| {
let mut tpk_bytes = [0u8; 32];
r.read_exact(&mut tpk_bytes)?;
secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
})?;
let enc_key = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
let nonce = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
Ok(WalletTKey {
keytype,
locked,
address,
tkey,
enc_key,
nonce,
})
}
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
out.write_u8(Self::serialized_version())?;
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
out.write_u8(self.locked as u8)?;
utils::write_string(&mut out, &self.address)?;
Optional::write(&mut out, &self.tkey, |w, pk|
w.write_all(&pk[..])
)?;
// Write enc_key
Optional::write(&mut out, &self.enc_key, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
// Write nonce
Optional::write(&mut out, &self.nonce, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))
}
pub fn lock(&mut self) -> io::Result<()> {
// For keys, encrypt the key into enckey
// assert that we have the encrypted key.
if self.enc_key.is_none() {
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when t-addr private key is not encrypted"));
}
self.tkey = None;
self.locked = true;
Ok(())
}
pub fn unlock(&mut self, key: &secretbox::Key) -> io::Result<()> {
// For imported keys, we need to decrypt from the encrypted key
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
let sk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
Ok(s) => s,
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
};
self.tkey = Some(secp256k1::SecretKey::from_slice(&sk_bytes[..]).map_err(|e|
io::Error::new(ErrorKind::InvalidData, format!("{}", e))
)?);
self.locked = false;
Ok(())
}
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
// For keys, encrypt the key into enckey
let nonce = secretbox::gen_nonce();
let sk_bytes = &self.tkey.unwrap()[..];
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
self.nonce = Some(nonce.as_ref().to_vec());
self.tkey = None;
// Also lock after encrypt
self.lock()
}
pub fn remove_encryption(&mut self) -> io::Result<()> {
if self.locked {
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
}
self.enc_key = None;
self.nonce = None;
Ok(())
}
}
#[derive(PartialEq, Debug, Clone)]
pub enum WalletZKeyType {
HdKey = 0,
ImportedSpendingKey = 1,
ImportedViewKey = 2
}
// A struct that holds z-address private keys or view keys
#[derive(Clone, Debug, PartialEq)]
pub struct WalletZKey {
pub(super) keytype: WalletZKeyType,
locked: bool,
pub(super) extsk: Option<ExtendedSpendingKey>,
pub(super) extfvk: ExtendedFullViewingKey,
pub(super) zaddress: PaymentAddress<Bls12>,
// If this is a HD key, what is the key number
pub(super) hdkey_num: Option<u32>,
// If locked, the encrypted private key is stored here
enc_key: Option<Vec<u8>>,
nonce: Option<Vec<u8>>,
}
impl WalletZKey {
pub fn new_hdkey(hdkey_num: u32, extsk: ExtendedSpendingKey) -> Self {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::HdKey,
locked: false,
extsk: Some(extsk),
extfvk,
zaddress,
hdkey_num: Some(hdkey_num),
enc_key: None,
nonce: None,
}
}
pub fn new_locked_hdkey(hdkey_num: u32, extfvk: ExtendedFullViewingKey) -> Self {
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::HdKey,
locked: true,
extsk: None,
extfvk,
zaddress,
hdkey_num: Some(hdkey_num),
enc_key: None,
nonce: None
}
}
pub fn new_imported_sk(extsk: ExtendedSpendingKey) -> Self {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::ImportedSpendingKey,
locked: false,
extsk: Some(extsk),
extfvk,
zaddress,
hdkey_num: None,
enc_key: None,
nonce: None,
}
}
pub fn new_imported_viewkey(extfvk: ExtendedFullViewingKey) -> Self {
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::ImportedViewKey,
locked: false,
extsk: None,
extfvk,
zaddress,
hdkey_num: None,
enc_key: None,
nonce: None,
}
}
fn serialized_version() -> u8 {
return 1;
}
pub fn have_spending_key(&self) -> bool {
self.extsk.is_some() || self.enc_key.is_some() || self.hdkey_num.is_some()
}
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
let version = inp.read_u8()?;
assert!(version <= Self::serialized_version());
let keytype: WalletZKeyType = match inp.read_u32::<LittleEndian>()? {
0 => Ok(WalletZKeyType::HdKey),
1 => Ok(WalletZKeyType::ImportedSpendingKey),
2 => Ok(WalletZKeyType::ImportedViewKey),
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown zkey type {}", n)))
}?;
let locked = inp.read_u8()? > 0;
let extsk = Optional::read(&mut inp, |r| ExtendedSpendingKey::read(r))?;
let extfvk = ExtendedFullViewingKey::read(&mut inp)?;
let zaddress = extfvk.default_address().unwrap().1;
let hdkey_num = Optional::read(&mut inp, |r| r.read_u32::<LittleEndian>())?;
let enc_key = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
let nonce = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
Ok(WalletZKey {
keytype,
locked,
extsk,
extfvk,
zaddress,
hdkey_num,
enc_key,
nonce,
})
}
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
out.write_u8(Self::serialized_version())?;
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
out.write_u8(self.locked as u8)?;
Optional::write(&mut out, &self.extsk, |w, sk| ExtendedSpendingKey::write(sk, w))?;
ExtendedFullViewingKey::write(&self.extfvk, &mut out)?;
Optional::write(&mut out, &self.hdkey_num, |o, n| o.write_u32::<LittleEndian>(*n))?;
// Write enc_key
Optional::write(&mut out, &self.enc_key, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
// Write nonce
Optional::write(&mut out, &self.nonce, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))
}
pub fn lock(&mut self) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, just empty out the keys, since they will be reconstructed from the hdkey_num
self.extsk = None;
self.locked = true;
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, encrypt the key into enckey
// assert that we have the encrypted key.
if self.enc_key.is_none() {
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when imported key is not encrypted"));
}
self.extsk = None;
self.locked = true;
},
WalletZKeyType::ImportedViewKey => {
// For viewing keys, there is nothing to lock, so just return true
self.locked = true;
}
}
Ok(())
}
pub fn unlock(&mut self, config: &LightClientConfig, bip39_seed: &[u8], key: &secretbox::Key) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
let (extsk, extfvk, address) =
LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed, self.hdkey_num.unwrap());
if address != self.zaddress {
return Err(io::Error::new(ErrorKind::InvalidData,
format!("zaddress mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), address, self.zaddress)));
}
if extfvk != self.extfvk {
return Err(io::Error::new(ErrorKind::InvalidData,
format!("fvk mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), extfvk, self.extfvk)));
}
self.extsk = Some(extsk);
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, we need to decrypt from the encrypted key
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
let extsk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
Ok(s) => s,
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
};
self.extsk = Some(ExtendedSpendingKey::read(&extsk_bytes[..])?);
},
WalletZKeyType::ImportedViewKey => {
// Viewing key unlocking is basically a no op
}
};
self.locked = false;
Ok(())
}
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, encrypt the key into enckey
let nonce = secretbox::gen_nonce();
let mut sk_bytes = vec![];
self.extsk.as_ref().unwrap().write(&mut sk_bytes)?;
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
self.nonce = Some(nonce.as_ref().to_vec());
},
WalletZKeyType::ImportedViewKey => {
// Encrypting a viewing key is a no-op
}
}
// Also lock after encrypt
self.lock()
}
pub fn remove_encryption(&mut self) -> io::Result<()> {
if self.locked {
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
}
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
Ok(())
},
WalletZKeyType::ImportedSpendingKey => {
self.enc_key = None;
self.nonce = None;
Ok(())
},
WalletZKeyType::ImportedViewKey => {
// Removing encryption is a no-op for viewing keys
Ok(())
}
}
}
}
#[cfg(test)]
pub mod tests {
use zcash_client_backend::{
encoding::{encode_payment_address, decode_extended_spending_key, decode_extended_full_viewing_key}
};
use sodiumoxide::crypto::secretbox;
use crate::lightclient::LightClientConfig;
use super::WalletZKey;
fn get_config() -> LightClientConfig {
LightClientConfig {
server: "0.0.0.0:0".parse().unwrap(),
chain_name: "main".to_string(),
sapling_activation_height: 0,
consensus_branch_id: "000000".to_string(),
anchor_offset: 0,
data_dir: None,
}
}
#[test]
fn test_serialize() {
let config = get_config();
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
let wzk = WalletZKey::new_imported_sk(esk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
let mut v: Vec<u8> = vec![];
// Serialize
wzk.write(&mut v).unwrap();
// Read it right back
let wzk2 = WalletZKey::read(&v[..]).unwrap();
{
assert_eq!(wzk, wzk2);
assert_eq!(wzk.extsk, wzk2.extsk);
assert_eq!(wzk.extfvk, wzk2.extfvk);
assert_eq!(wzk.zaddress, wzk2.zaddress);
}
}
#[test]
fn test_encrypt_decrypt_sk() {
let config = get_config();
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
let mut wzk = WalletZKey::new_imported_sk(esk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
// Can't lock without encryption
assert!(wzk.lock().is_err());
// Encryption key
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
// Encrypt, but save the extsk first
let orig_extsk = wzk.extsk.clone().unwrap();
wzk.encrypt(&key).unwrap();
{
assert!(wzk.enc_key.is_some());
assert!(wzk.nonce.is_some());
}
// Now lock
assert!(wzk.lock().is_ok());
{
assert!(wzk.extsk.is_none());
assert_eq!(wzk.locked, true);
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
}
// Can't remove encryption without unlocking
assert!(wzk.remove_encryption().is_err());
// Unlock
assert!(wzk.unlock(&config, &[], &key).is_ok());
{
assert_eq!(wzk.extsk, Some(orig_extsk));
}
// Remove encryption
assert!(wzk.remove_encryption().is_ok());
{
assert_eq!(wzk.enc_key, None);
assert_eq!(wzk.nonce, None);
}
}
#[test]
fn test_encrypt_decrypt_vk() {
let config = get_config();
// Priv Key's address is "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc"
let viewkey = "zxviews1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3d9rr3ydjm9zl9464majh4pa3ejkfy779dm38sfnkar67et7ykxkk0z9rfsmf9jclfj2k85xt2exkg4pu5xqyzyxzlqa6x3p9wrd7pwdq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3ycqtvvje";
let extfvk = decode_extended_full_viewing_key(config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap();
let mut wzk = WalletZKey::new_imported_viewkey(extfvk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc".to_string());
// Encryption key
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
// Encrypt
wzk.encrypt(&key).unwrap();
{
assert!(wzk.enc_key.is_none());
assert!(wzk.nonce.is_none());
}
// Now lock
assert!(wzk.lock().is_ok());
{
assert!(wzk.extsk.is_none());
assert_eq!(wzk.locked, true);
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
}
// Can't remove encryption without unlocking
assert!(wzk.remove_encryption().is_err());
// Unlock
assert!(wzk.unlock(&config, &[], &key).is_ok());
{
assert_eq!(wzk.extsk, None);
}
// Remove encryption
assert!(wzk.remove_encryption().is_ok());
{
assert_eq!(wzk.enc_key, None);
assert_eq!(wzk.nonce, None);
}
}
}