Merge pull request #8 from zcash/backend-updates

Backend updates
This commit is contained in:
str4d 2019-02-11 09:44:46 -05:00 committed by GitHub
commit 5a67bdae79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2901 additions and 90 deletions

880
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,38 +11,53 @@ publish = false
[dependencies]
android_logger = "0.6"
failure = "0.1"
futures = { version = "0.1", optional = true }
grpc = { version = "0.6", optional = true }
hex = { version = "0.3", optional = true }
jni = { version = "0.10", default-features = false }
log = "0.4"
log-panics = "2.0.0"
protobuf = "2"
rand = "0.4"
rusqlite = { version = "0.15", features = ["bundled"] }
time = "0.1"
[dependencies.ff]
git = "https://github.com/str4d/librustzcash.git"
rev = "89cfef8515d5d88809c485a44fdc54572b9e5666"
rev = "669df0fa7b2fbd8e1d2ee78d7f0c6544ea04bfd3"
[dependencies.pairing]
git = "https://github.com/str4d/librustzcash.git"
rev = "89cfef8515d5d88809c485a44fdc54572b9e5666"
rev = "669df0fa7b2fbd8e1d2ee78d7f0c6544ea04bfd3"
[dependencies.sapling-crypto]
git = "https://github.com/str4d/librustzcash.git"
rev = "89cfef8515d5d88809c485a44fdc54572b9e5666"
rev = "669df0fa7b2fbd8e1d2ee78d7f0c6544ea04bfd3"
[dependencies.zcash_client_backend]
git = "https://github.com/str4d/librustzcash.git"
rev = "89cfef8515d5d88809c485a44fdc54572b9e5666"
rev = "669df0fa7b2fbd8e1d2ee78d7f0c6544ea04bfd3"
[dependencies.zcash_primitives]
git = "https://github.com/str4d/librustzcash.git"
rev = "89cfef8515d5d88809c485a44fdc54572b9e5666"
rev = "669df0fa7b2fbd8e1d2ee78d7f0c6544ea04bfd3"
[dependencies.zip32]
git = "https://github.com/str4d/librustzcash.git"
rev = "89cfef8515d5d88809c485a44fdc54572b9e5666"
rev = "669df0fa7b2fbd8e1d2ee78d7f0c6544ea04bfd3"
[dev-dependencies]
tempfile = "3"
[features]
updater = ["futures", "grpc", "hex"]
[lib]
name = "zcashwalletsdk"
path = "src/main/rust/lib.rs"
crate-type = ["staticlib", "cdylib"]
[[bin]]
name = "update-sapling-tree"
path = "src/main/rust/bin/update_sapling_tree.rs"
required-features = ["updater"]

View File

@ -19,6 +19,10 @@ class JniConverter {
external fun getBalance(dbData: String, account: Int): Long
external fun getReceivedMemoAsUtf8(dbData: String, idNote: Long): String
external fun getSentMemoAsUtf8(dbData: String, idNote: Long): String
external fun scanBlocks(db_cache: String, db_data: String): Boolean
external fun sendToAddress(

View File

@ -0,0 +1,47 @@
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = "walletrpc";
// 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;
uint32 time = 4;
bytes header = 5; // (hash and time) OR (full header)
repeated CompactTx vtx = 6; // 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;
bytes hash = 2;
// 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 CompactSpend spends = 4;
repeated CompactOutput outputs = 5;
}
message CompactSpend {
bytes nf = 1;
}
message CompactOutput {
bytes cmu = 1;
bytes epk = 2;
bytes ciphertext = 3;
}

View File

@ -0,0 +1,49 @@
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = "walletrpc";
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.
message RawTransaction {
bytes data = 1;
}
message SendResponse {
int32 errorCode = 1;
string errorMessage = 2;
}
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
message ChainSpec {}
service CompactTxStreamer {
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
rpc GetBlock(BlockID) returns (CompactBlock) {}
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
}

1231
src/main/rust/bin/service.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,180 @@
// This file is generated. Do not edit
// @generated
// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy)]
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]
// interface
pub trait CompactTxStreamer {
fn get_latest_block(&self, o: ::grpc::RequestOptions, p: super::service::ChainSpec) -> ::grpc::SingleResponse<super::service::BlockID>;
fn get_block(&self, o: ::grpc::RequestOptions, p: super::service::BlockID) -> ::grpc::SingleResponse<super::compact_formats::CompactBlock>;
fn get_block_range(&self, o: ::grpc::RequestOptions, p: super::service::BlockRange) -> ::grpc::StreamingResponse<super::compact_formats::CompactBlock>;
fn get_transaction(&self, o: ::grpc::RequestOptions, p: super::service::TxFilter) -> ::grpc::SingleResponse<super::service::RawTransaction>;
fn send_transaction(&self, o: ::grpc::RequestOptions, p: super::service::RawTransaction) -> ::grpc::SingleResponse<super::service::SendResponse>;
}
// client
pub struct CompactTxStreamerClient {
grpc_client: ::std::sync::Arc<::grpc::Client>,
method_GetLatestBlock: ::std::sync::Arc<::grpc::rt::MethodDescriptor<super::service::ChainSpec, super::service::BlockID>>,
method_GetBlock: ::std::sync::Arc<::grpc::rt::MethodDescriptor<super::service::BlockID, super::compact_formats::CompactBlock>>,
method_GetBlockRange: ::std::sync::Arc<::grpc::rt::MethodDescriptor<super::service::BlockRange, super::compact_formats::CompactBlock>>,
method_GetTransaction: ::std::sync::Arc<::grpc::rt::MethodDescriptor<super::service::TxFilter, super::service::RawTransaction>>,
method_SendTransaction: ::std::sync::Arc<::grpc::rt::MethodDescriptor<super::service::RawTransaction, super::service::SendResponse>>,
}
impl ::grpc::ClientStub for CompactTxStreamerClient {
fn with_client(grpc_client: ::std::sync::Arc<::grpc::Client>) -> Self {
CompactTxStreamerClient {
grpc_client: grpc_client,
method_GetLatestBlock: ::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
method_GetBlock: ::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
method_GetBlockRange: ::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange".to_string(),
streaming: ::grpc::rt::GrpcStreaming::ServerStreaming,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
method_GetTransaction: ::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
method_SendTransaction: ::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
}
}
}
impl CompactTxStreamer for CompactTxStreamerClient {
fn get_latest_block(&self, o: ::grpc::RequestOptions, p: super::service::ChainSpec) -> ::grpc::SingleResponse<super::service::BlockID> {
self.grpc_client.call_unary(o, p, self.method_GetLatestBlock.clone())
}
fn get_block(&self, o: ::grpc::RequestOptions, p: super::service::BlockID) -> ::grpc::SingleResponse<super::compact_formats::CompactBlock> {
self.grpc_client.call_unary(o, p, self.method_GetBlock.clone())
}
fn get_block_range(&self, o: ::grpc::RequestOptions, p: super::service::BlockRange) -> ::grpc::StreamingResponse<super::compact_formats::CompactBlock> {
self.grpc_client.call_server_streaming(o, p, self.method_GetBlockRange.clone())
}
fn get_transaction(&self, o: ::grpc::RequestOptions, p: super::service::TxFilter) -> ::grpc::SingleResponse<super::service::RawTransaction> {
self.grpc_client.call_unary(o, p, self.method_GetTransaction.clone())
}
fn send_transaction(&self, o: ::grpc::RequestOptions, p: super::service::RawTransaction) -> ::grpc::SingleResponse<super::service::SendResponse> {
self.grpc_client.call_unary(o, p, self.method_SendTransaction.clone())
}
}
// server
pub struct CompactTxStreamerServer;
impl CompactTxStreamerServer {
pub fn new_service_def<H : CompactTxStreamer + 'static + Sync + Send + 'static>(handler: H) -> ::grpc::rt::ServerServiceDefinition {
let handler_arc = ::std::sync::Arc::new(handler);
::grpc::rt::ServerServiceDefinition::new("/cash.z.wallet.sdk.rpc.CompactTxStreamer",
vec![
::grpc::rt::ServerMethod::new(
::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
{
let handler_copy = handler_arc.clone();
::grpc::rt::MethodHandlerUnary::new(move |o, p| handler_copy.get_latest_block(o, p))
},
),
::grpc::rt::ServerMethod::new(
::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
{
let handler_copy = handler_arc.clone();
::grpc::rt::MethodHandlerUnary::new(move |o, p| handler_copy.get_block(o, p))
},
),
::grpc::rt::ServerMethod::new(
::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange".to_string(),
streaming: ::grpc::rt::GrpcStreaming::ServerStreaming,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
{
let handler_copy = handler_arc.clone();
::grpc::rt::MethodHandlerServerStreaming::new(move |o, p| handler_copy.get_block_range(o, p))
},
),
::grpc::rt::ServerMethod::new(
::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
{
let handler_copy = handler_arc.clone();
::grpc::rt::MethodHandlerUnary::new(move |o, p| handler_copy.get_transaction(o, p))
},
),
::grpc::rt::ServerMethod::new(
::std::sync::Arc::new(::grpc::rt::MethodDescriptor {
name: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction".to_string(),
streaming: ::grpc::rt::GrpcStreaming::Unary,
req_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
resp_marshaller: Box::new(::grpc::protobuf::MarshallerProtobuf),
}),
{
let handler_copy = handler_arc.clone();
::grpc::rt::MethodHandlerUnary::new(move |o, p| handler_copy.send_transaction(o, p))
},
),
],
)
}
}

View File

@ -0,0 +1,101 @@
extern crate ff;
extern crate futures;
extern crate grpc;
extern crate hex;
extern crate pairing;
extern crate protobuf;
extern crate zcash_client_backend;
extern crate zcash_primitives;
mod service;
mod service_grpc;
use ff::{PrimeField, PrimeFieldRepr};
use futures::Stream;
use grpc::ClientStubExt;
use pairing::bls12_381::{Fr, FrRepr};
use zcash_client_backend::proto::compact_formats;
use zcash_primitives::merkle_tree::{CommitmentTree, Node};
use service_grpc::CompactTxStreamer;
const LIGHTWALLETD_HOST: &str = "lightwalletd.z.cash";
const LIGHTWALLETD_PORT: u16 = 9067;
const BATCH_SIZE: u64 = 10_000;
fn print_sapling_tree(height: u64, time: u32, tree: CommitmentTree) {
let mut tree_bytes = vec![];
tree.write(&mut tree_bytes).unwrap();
println!("Updated Sapling tree:");
println!("- Height: {}", height);
println!("- Time: {}", time);
println!("- Tree: {}", hex::encode(tree_bytes));
}
fn main() {
// For now, start from Sapling activation height
let mut start_height = 280000;
let mut tree = CommitmentTree::new();
let client_conf = Default::default();
let client = service_grpc::CompactTxStreamerClient::new_plain(
LIGHTWALLETD_HOST,
LIGHTWALLETD_PORT,
client_conf,
)
.unwrap();
loop {
// Get the latest height
let latest_height = client
.get_latest_block(grpc::RequestOptions::new(), service::ChainSpec::new())
.wait_drop_metadata()
.unwrap()
.height;
let end_height = if latest_height - start_height < BATCH_SIZE {
latest_height
} else {
start_height + BATCH_SIZE - 1
};
// Request the next batch of blocks
println!("Fetching blocks {}..{}", start_height, end_height);
let mut start = service::BlockID::new();
start.set_height(start_height);
let mut end = service::BlockID::new();
end.set_height(end_height);
let mut range = service::BlockRange::new();
range.set_start(start);
range.set_end(end);
let blocks = client
.get_block_range(grpc::RequestOptions::new(), range)
.drop_metadata()
.wait();
let mut end_time = 0;
let mut parsed = 0;
for block in blocks {
let block = block.unwrap();
end_time = block.time;
for tx in block.vtx.iter() {
for output in tx.outputs.iter() {
// Append commitment to tree
let mut repr = FrRepr::default();
repr.read_le(&output.cmu[..]).unwrap();
let cmu = Fr::from_repr(repr).unwrap();
let node = Node::new(cmu.into_repr());
tree.append(node).unwrap();
}
}
parsed += 1
}
println!("Parsed {} blocks", parsed);
if end_height == latest_height {
print_sapling_tree(end_height, end_time, tree);
break;
} else {
start_height = end_height + 1
}
}
}

View File

@ -9,12 +9,17 @@ extern crate jni;
extern crate log_panics;
extern crate pairing;
extern crate protobuf;
extern crate rand;
extern crate rusqlite;
extern crate sapling_crypto;
extern crate time;
extern crate zcash_client_backend;
extern crate zcash_primitives;
extern crate zip32;
#[cfg(test)]
extern crate tempfile;
mod sql;
const SAPLING_CONSENSUS_BRANCH_ID: u32 = 0x76b8_09bb;
@ -228,6 +233,56 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getBalance(
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getReceivedMemoAsUtf8(
env: JNIEnv,
_: JClass,
db_data: JString,
id_note: jlong,
) -> jstring {
let db_data: String = env
.get_string(db_data)
.expect("Couldn't get Java string!")
.into();
let memo = match crate::sql::get_received_memo_as_utf8(db_data, id_note) {
Ok(memo) => memo.unwrap_or_default(),
Err(e) => {
error!("Error while fetching memo: {}", e);
// Return an empty string to indicate an error
String::default()
}
};
let output = env.new_string(memo).expect("Couldn't create Java string!");
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getSentMemoAsUtf8(
env: JNIEnv,
_: JClass,
db_data: JString,
id_note: jlong,
) -> jstring {
let db_data: String = env
.get_string(db_data)
.expect("Couldn't get Java string!")
.into();
let memo = match crate::sql::get_sent_memo_as_utf8(db_data, id_note) {
Ok(memo) => memo.unwrap_or_default(),
Err(e) => {
error!("Error while fetching memo: {}", e);
// Return an empty string to indicate an error
String::default()
}
};
let output = env.new_string(memo).expect("Couldn't create Java string!");
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_scanBlocks(
env: JNIEnv,
@ -316,8 +371,8 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_sendToAddress(
let memo = match Memo::from_str(&memo) {
Ok(memo) => Some(memo),
Err(()) => {
error!("Memo is too long");
Err(e) => {
error!("{}", e);
return -1;
}
};

View File

@ -7,6 +7,7 @@ use sapling_crypto::{
jubjub::fs::{Fs, FsRepr},
primitives::{Diversifier, Note, PaymentAddress},
};
use std::path::Path;
use zcash_client_backend::{
constants::{HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, HRP_SAPLING_PAYMENT_ADDRESS_TEST},
encoding::{
@ -32,7 +33,19 @@ fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String {
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &addr)
}
pub fn init_data_database(db_data: &str) -> rusqlite::Result<()> {
pub fn init_cache_database<P: AsRef<Path>>(db_cache: P) -> rusqlite::Result<()> {
let cache = Connection::open(db_cache)?;
cache.execute(
"CREATE TABLE IF NOT EXISTS compactblocks (
height INTEGER PRIMARY KEY,
data BLOB NOT NULL
)",
NO_PARAMS,
)?;
Ok(())
}
pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> rusqlite::Result<()> {
let data = Connection::open(db_data)?;
data.execute(
"CREATE TABLE IF NOT EXISTS accounts (
@ -54,8 +67,10 @@ pub fn init_data_database(db_data: &str) -> rusqlite::Result<()> {
"CREATE TABLE IF NOT EXISTS transactions (
id_tx INTEGER PRIMARY KEY,
txid BLOB NOT NULL UNIQUE,
created TEXT,
block INTEGER,
tx_index INTEGER,
expiry_height INTEGER,
raw BLOB,
FOREIGN KEY (block) REFERENCES blocks(height)
)",
@ -111,7 +126,10 @@ pub fn init_data_database(db_data: &str) -> rusqlite::Result<()> {
Ok(())
}
pub fn init_accounts_table(db_data: &str, extfvks: &[ExtendedFullViewingKey]) -> Result<(), Error> {
pub fn init_accounts_table<P: AsRef<Path>>(
db_data: P,
extfvks: &[ExtendedFullViewingKey],
) -> Result<(), Error> {
let data = Connection::open(db_data)?;
let mut empty_check = data.prepare("SELECT * FROM accounts LIMIT 1")?;
@ -140,8 +158,8 @@ pub fn init_accounts_table(db_data: &str, extfvks: &[ExtendedFullViewingKey]) ->
Ok(())
}
pub fn init_blocks_table(
db_data: &str,
pub fn init_blocks_table<P: AsRef<Path>>(
db_data: P,
height: i32,
time: u32,
sapling_tree: &[u8],
@ -173,7 +191,7 @@ struct WitnessRow {
witness: IncrementalWitness,
}
pub fn get_address(db_data: &str, account: u32) -> Result<String, Error> {
pub fn get_address<P: AsRef<Path>>(db_data: P, account: u32) -> Result<String, Error> {
let data = Connection::open(db_data)?;
let addr = data.query_row(
@ -186,7 +204,7 @@ pub fn get_address(db_data: &str, account: u32) -> Result<String, Error> {
Ok(addr)
}
pub fn get_balance(db_data: &str, account: u32) -> Result<Amount, Error> {
pub fn get_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, Error> {
let data = Connection::open(db_data)?;
let balance = data.query_row(
@ -199,11 +217,46 @@ pub fn get_balance(db_data: &str, account: u32) -> Result<Amount, Error> {
Ok(Amount(balance))
}
pub fn get_received_memo_as_utf8<P: AsRef<Path>>(
db_data: P,
id_note: i64,
) -> Result<Option<String>, Error> {
let data = Connection::open(db_data)?;
let memo: Vec<_> = data.query_row(
"SELECT memo FROM received_notes
WHERE id_note = ?",
&[id_note],
|row| row.get(0),
)?;
Memo::from_bytes(&memo).and_then(|memo| memo.to_utf8())
}
pub fn get_sent_memo_as_utf8<P: AsRef<Path>>(
db_data: P,
id_note: i64,
) -> Result<Option<String>, Error> {
let data = Connection::open(db_data)?;
let memo: Vec<_> = data.query_row(
"SELECT memo FROM sent_notes
WHERE id_note = ?",
&[id_note],
|row| row.get(0),
)?;
Memo::from_bytes(&memo).and_then(|memo| memo.to_utf8())
}
/// Scans new blocks added to the cache for any transactions received by the
/// tracked accounts.
///
/// Assumes that the caller is handling rollbacks.
pub fn scan_cached_blocks(db_cache: &str, db_data: &str) -> Result<(), Error> {
pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
db_cache: P,
db_data: Q,
) -> Result<(), Error> {
let cache = Connection::open(db_cache)?;
let data = Connection::open(db_data)?;
@ -272,21 +325,21 @@ pub fn scan_cached_blocks(db_cache: &str, db_data: &str) -> Result<(), Error> {
let mut tree = match stmt_fetch_tree.query_row(&[last_height], |row| match row.get_checked(0) {
Ok(data) => {
let data: Vec<_> = data;
CommitmentTree::read(&data[..]).unwrap()
CommitmentTree::read(&data[..])
}
Err(_) => CommitmentTree::new(),
Err(_) => Ok(CommitmentTree::new()),
}) {
Ok(tree) => tree,
Err(_) => CommitmentTree::new(),
};
Err(_) => Ok(CommitmentTree::new()),
}?;
// Get most recent incremental witnesses for the notes we are tracking
let witnesses = stmt_fetch_witnesses.query_map(&[last_height], |row| {
let data: Vec<_> = row.get(1);
WitnessRow {
IncrementalWitness::read(&data[..]).map(|witness| WitnessRow {
id_note: row.get(0),
witness: IncrementalWitness::read(&data[..]).unwrap(),
}
witness,
})
})?;
let mut witnesses: Vec<_> = witnesses.collect::<Result<_, _>>()?;
@ -334,7 +387,8 @@ pub fn scan_cached_blocks(db_cache: &str, db_data: &str) -> Result<(), Error> {
// Insert the block into the database.
let mut encoded_tree = Vec::new();
tree.write(&mut encoded_tree).unwrap();
tree.write(&mut encoded_tree)
.expect("Should be able to write to a Vec");
stmt_insert_block.execute(&[
row.height.to_sql()?,
block_time.to_sql()?,
@ -420,7 +474,10 @@ pub fn scan_cached_blocks(db_cache: &str, db_data: &str) -> Result<(), Error> {
let mut encoded = Vec::new();
for witness_row in witnesses.iter() {
encoded.clear();
witness_row.witness.write(&mut encoded).unwrap();
witness_row
.witness
.write(&mut encoded)
.expect("Should be able to write to a Vec");
stmt_insert_witness.execute(&[
witness_row.id_note.to_sql()?,
last_height.to_sql()?,
@ -445,8 +502,8 @@ struct SelectedNoteRow {
}
/// Creates a transaction paying the specified address.
pub fn send_to_address(
db_data: &str,
pub fn send_to_address<P: AsRef<Path>>(
db_data: P,
consensus_branch_id: u32,
prover: impl TxProver,
(account, extsk): (u32, &ExtendedSpendingKey),
@ -573,21 +630,30 @@ pub fn send_to_address(
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())?;
let (tx, tx_metadata) = builder.build(consensus_branch_id, prover)?;
// We only called add_sapling_output() once.
let output_index = tx_metadata.output_index(0).unwrap() as i64;
let output_index = match tx_metadata.output_index(0) {
Some(idx) => idx as i64,
None => panic!("Output 0 should exist in the transaction"),
};
let created = time::get_time();
// Save the transaction in the database.
let mut raw_tx = vec![];
tx.write(&mut raw_tx)?;
let mut stmt_insert_tx = data.prepare(
"INSERT INTO transactions (txid, raw)
VALUES (?, ?)",
"INSERT INTO transactions (txid, created, expiry_height, raw)
VALUES (?, ?, ?, ?)",
)?;
stmt_insert_tx.execute(&[&tx.txid().0[..], &raw_tx[..]])?;
stmt_insert_tx.execute(&[
tx.txid().0.to_sql()?,
created.to_sql()?,
tx.expiry_height.to_sql()?,
raw_tx.to_sql()?,
])?;
let id_tx = data.last_insert_rowid();
// Save the sent note in the database.
let to_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, to);
if memo.is_some() {
if let Some(memo) = memo {
let mut stmt_insert_sent_note = data.prepare(
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
VALUES (?, ?, ?, ?, ?, ?)",
@ -598,7 +664,7 @@ pub fn send_to_address(
account.to_sql()?,
to_str.to_sql()?,
value.0.to_sql()?,
memo.unwrap().as_bytes().to_sql()?,
memo.as_bytes().to_sql()?,
])?;
} else {
let mut stmt_insert_sent_note = data.prepare(
@ -617,3 +683,302 @@ pub fn send_to_address(
// Return the row number of the transaction, so the caller can fetch it for sending.
Ok(id_tx)
}
#[cfg(test)]
mod tests {
use ff::{PrimeField, PrimeFieldRepr};
use pairing::bls12_381::Bls12;
use protobuf::Message;
use rand::{thread_rng, Rand, Rng};
use rusqlite::{types::ToSql, Connection};
use sapling_crypto::{
jubjub::fs::Fs,
primitives::{Note, PaymentAddress},
};
use std::path::Path;
use tempfile::NamedTempFile;
use zcash_client_backend::{
constants::HRP_SAPLING_PAYMENT_ADDRESS_TEST,
encoding::decode_payment_address,
note_encryption::{Memo, SaplingNoteEncryption},
proto::compact_formats::{CompactBlock, CompactOutput, CompactSpend, CompactTx},
};
use zcash_primitives::{transaction::components::Amount, JUBJUB};
use zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
use super::{
get_address, get_balance, init_accounts_table, init_blocks_table, init_cache_database,
init_data_database, scan_cached_blocks,
};
/// Create a fake CompactBlock at the given height, containing a single output paying
/// the given address. Returns the CompactBlock and the nullifier for the new note.
fn fake_compact_block(
height: i32,
extfvk: ExtendedFullViewingKey,
value: Amount,
) -> (CompactBlock, Vec<u8>) {
let to = extfvk.default_address().unwrap().1;
// Create a fake Note for the account
let mut rng = thread_rng();
let note = Note {
g_d: to.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
pk_d: to.pk_d.clone(),
value: value.0 as u64,
r: Fs::rand(&mut rng),
};
let encryptor =
SaplingNoteEncryption::new(extfvk.fvk.ovk, note.clone(), to.clone(), Memo::default());
let mut cmu = vec![];
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
let mut epk = vec![];
encryptor.epk().write(&mut epk).unwrap();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
// Create a fake CompactBlock containing the note
let mut cout = CompactOutput::new();
cout.set_cmu(cmu);
cout.set_epk(epk);
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
let mut ctx = CompactTx::new();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.set_hash(txid);
ctx.outputs.push(cout);
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.vtx.push(ctx);
(cb, note.nf(&extfvk.fvk.vk, 0, &JUBJUB))
}
/// Create a fake CompactBlock at the given height, spending a single note from the
/// given address.
fn fake_compact_block_spending(
height: i32,
(nf, in_value): (Vec<u8>, Amount),
extfvk: ExtendedFullViewingKey,
to: PaymentAddress<Bls12>,
value: Amount,
) -> CompactBlock {
let mut rng = thread_rng();
// Create a fake CompactBlock containing the note
let mut cspend = CompactSpend::new();
cspend.set_nf(nf);
let mut ctx = CompactTx::new();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.set_hash(txid);
ctx.spends.push(cspend);
// Create a fake Note for the payment
ctx.outputs.push({
let note = Note {
g_d: to.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
pk_d: to.pk_d.clone(),
value: value.0 as u64,
r: Fs::rand(&mut rng),
};
let encryptor =
SaplingNoteEncryption::new(extfvk.fvk.ovk, note.clone(), to, Memo::default());
let mut cmu = vec![];
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
let mut epk = vec![];
encryptor.epk().write(&mut epk).unwrap();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
let mut cout = CompactOutput::new();
cout.set_cmu(cmu);
cout.set_epk(epk);
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
cout
});
// Create a fake Note for the change
ctx.outputs.push({
let change_addr = extfvk.default_address().unwrap().1;
let note = Note {
g_d: change_addr.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
pk_d: change_addr.pk_d.clone(),
value: (in_value.0 - value.0) as u64,
r: Fs::rand(&mut rng),
};
let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk,
note.clone(),
change_addr,
Memo::default(),
);
let mut cmu = vec![];
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
let mut epk = vec![];
encryptor.epk().write(&mut epk).unwrap();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
let mut cout = CompactOutput::new();
cout.set_cmu(cmu);
cout.set_epk(epk);
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
cout
});
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.vtx.push(ctx);
cb
}
/// Insert a fake CompactBlock into the cache DB.
fn insert_into_cache<P: AsRef<Path>>(db_cache: P, cb: &CompactBlock) {
let cb_bytes = cb.write_to_bytes().unwrap();
let cache = Connection::open(&db_cache).unwrap();
cache
.prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")
.unwrap()
.execute(&[
(cb.height as i32).to_sql().unwrap(),
cb_bytes.to_sql().unwrap(),
])
.unwrap();
}
#[test]
fn init_accounts_table_only_works_once() {
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// We can call the function as many times as we want with no data
init_accounts_table(&db_data, &[]).unwrap();
init_accounts_table(&db_data, &[]).unwrap();
// First call with data should initialise the accounts table
let extfvks = [ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(
&[],
))];
init_accounts_table(&db_data, &extfvks).unwrap();
// Subsequent calls should return an error
init_accounts_table(&db_data, &[]).unwrap_err();
init_accounts_table(&db_data, &extfvks).unwrap_err();
}
#[test]
fn init_blocks_table_only_works_once() {
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// First call with data should initialise the blocks table
init_blocks_table(&db_data, 1, 1, &[]).unwrap();
// Subsequent calls should return an error
init_blocks_table(&db_data, 2, 2, &[]).unwrap_err();
}
#[test]
fn init_accounts_table_stores_correct_address() {
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
init_accounts_table(&db_data, &extfvks).unwrap();
// The account's address should be in the data DB
let addr = get_address(&db_data, 0).unwrap();
let pa = decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &addr).unwrap();
assert_eq!(pa, extsk.default_address().unwrap().1);
}
#[test]
fn scan_cached_blocks_finds_received_notes() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = cache_file.path();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
// Account balance should be zero
assert_eq!(get_balance(db_data, 0).unwrap(), Amount(0));
// Create a fake CompactBlock sending value to the address
let value = Amount(5);
let (cb, _) = fake_compact_block(1, extfvk.clone(), value);
insert_into_cache(db_cache, &cb);
// Scan the cache
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should reflect the received note
assert_eq!(get_balance(db_data, 0).unwrap(), value);
// Create a second fake CompactBlock sending more value to the address
let value2 = Amount(7);
let (cb2, _) = fake_compact_block(2, extfvk, value2);
insert_into_cache(db_cache, &cb2);
// Scan the cache again
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should reflect both received notes
// TODO: impl Sum for Amount
assert_eq!(get_balance(db_data, 0).unwrap(), Amount(value.0 + value2.0));
}
#[test]
fn scan_cached_blocks_finds_change_notes() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = cache_file.path();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
// Account balance should be zero
assert_eq!(get_balance(db_data, 0).unwrap(), Amount(0));
// Create a fake CompactBlock sending value to the address
let value = Amount(5);
let (cb, nf) = fake_compact_block(1, extfvk.clone(), value);
insert_into_cache(db_cache, &cb);
// Scan the cache
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should reflect the received note
assert_eq!(get_balance(db_data, 0).unwrap(), value);
// Create a second fake CompactBlock spending value from the address
let extsk2 = ExtendedSpendingKey::master(&[0]);
let to2 = extsk2.default_address().unwrap().1;
let value2 = Amount(2);
insert_into_cache(
db_cache,
&fake_compact_block_spending(2, (nf, value), extfvk, to2, value2),
);
// Scan the cache again
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should equal the change
// TODO: impl Sum for Amount
assert_eq!(get_balance(db_data, 0).unwrap(), Amount(value.0 - value2.0));
}
}