mirror of
https://github.com/Qortal/pirate-librustzcash.git
synced 2025-02-01 08:12:14 +00:00
zcash_client_sqlite::transact::create_to_address()
This commit is contained in:
parent
9a742d25ea
commit
4c1237fa50
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -674,8 +674,10 @@ dependencies = [
|
|||||||
"rand_os 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand_os 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"zcash_client_backend 0.0.0",
|
"zcash_client_backend 0.0.0",
|
||||||
"zcash_primitives 0.0.0",
|
"zcash_primitives 0.0.0",
|
||||||
|
"zcash_proofs 0.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2,3 +2,7 @@
|
|||||||
|
|
||||||
pub mod mainnet;
|
pub mod mainnet;
|
||||||
pub mod testnet;
|
pub mod testnet;
|
||||||
|
|
||||||
|
pub const SPROUT_CONSENSUS_BRANCH_ID: u32 = 0;
|
||||||
|
pub const OVERWINTER_CONSENSUS_BRANCH_ID: u32 = 0x5ba8_1b19;
|
||||||
|
pub const SAPLING_CONSENSUS_BRANCH_ID: u32 = 0x76b8_09bb;
|
||||||
|
@ -9,13 +9,15 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bech32 = "0.7"
|
bech32 = "0.7"
|
||||||
ff = { path = "../ff" }
|
ff = { path = "../ff" }
|
||||||
|
pairing = { path = "../pairing" }
|
||||||
protobuf = "2"
|
protobuf = "2"
|
||||||
rusqlite = { version = "0.20", features = ["bundled"] }
|
rusqlite = { version = "0.20", features = ["bundled"] }
|
||||||
|
time = "0.1"
|
||||||
zcash_client_backend = { path = "../zcash_client_backend" }
|
zcash_client_backend = { path = "../zcash_client_backend" }
|
||||||
zcash_primitives = { path = "../zcash_primitives" }
|
zcash_primitives = { path = "../zcash_primitives" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pairing = { path = "../pairing" }
|
|
||||||
rand_core = "0.5"
|
rand_core = "0.5"
|
||||||
rand_os = "0.2"
|
rand_os = "0.2"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
zcash_proofs = { path = "../zcash_proofs" }
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
use std::error;
|
use std::error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use zcash_primitives::{sapling::Node, transaction::TxId};
|
use zcash_primitives::{
|
||||||
|
sapling::Node,
|
||||||
|
transaction::{builder, TxId},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
CorruptedData(&'static str),
|
CorruptedData(&'static str),
|
||||||
IncorrectHRPExtFVK,
|
IncorrectHRPExtFVK,
|
||||||
|
InsufficientBalance(u64, u64),
|
||||||
|
InvalidExtSK(u32),
|
||||||
InvalidHeight(i32, i32),
|
InvalidHeight(i32, i32),
|
||||||
|
InvalidMemo(std::str::Utf8Error),
|
||||||
InvalidNewWitnessAnchor(usize, TxId, i32, Node),
|
InvalidNewWitnessAnchor(usize, TxId, i32, Node),
|
||||||
|
InvalidNote,
|
||||||
InvalidWitnessAnchor(i64, i32),
|
InvalidWitnessAnchor(i64, i32),
|
||||||
ScanRequired,
|
ScanRequired,
|
||||||
TableNotEmpty,
|
TableNotEmpty,
|
||||||
Bech32(bech32::Error),
|
Bech32(bech32::Error),
|
||||||
|
Builder(builder::Error),
|
||||||
Database(rusqlite::Error),
|
Database(rusqlite::Error),
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Protobuf(protobuf::ProtobufError),
|
Protobuf(protobuf::ProtobufError),
|
||||||
@ -25,16 +33,26 @@ impl fmt::Display for Error {
|
|||||||
match &self.0 {
|
match &self.0 {
|
||||||
ErrorKind::CorruptedData(reason) => write!(f, "Data DB is corrupted: {}", reason),
|
ErrorKind::CorruptedData(reason) => write!(f, "Data DB is corrupted: {}", reason),
|
||||||
ErrorKind::IncorrectHRPExtFVK => write!(f, "Incorrect HRP for extfvk"),
|
ErrorKind::IncorrectHRPExtFVK => write!(f, "Incorrect HRP for extfvk"),
|
||||||
|
ErrorKind::InsufficientBalance(have, need) => write!(
|
||||||
|
f,
|
||||||
|
"Insufficient balance (have {}, need {} including fee)",
|
||||||
|
have, need
|
||||||
|
),
|
||||||
|
ErrorKind::InvalidExtSK(account) => {
|
||||||
|
write!(f, "Incorrect ExtendedSpendingKey for account {}", account)
|
||||||
|
}
|
||||||
ErrorKind::InvalidHeight(expected, actual) => write!(
|
ErrorKind::InvalidHeight(expected, actual) => write!(
|
||||||
f,
|
f,
|
||||||
"Expected height of next CompactBlock to be {}, but was {}",
|
"Expected height of next CompactBlock to be {}, but was {}",
|
||||||
expected, actual
|
expected, actual
|
||||||
),
|
),
|
||||||
|
ErrorKind::InvalidMemo(e) => write!(f, "{}", e),
|
||||||
ErrorKind::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!(
|
ErrorKind::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!(
|
||||||
f,
|
f,
|
||||||
"New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}",
|
"New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}",
|
||||||
output, txid, last_height, anchor,
|
output, txid, last_height, anchor,
|
||||||
),
|
),
|
||||||
|
ErrorKind::InvalidNote => write!(f, "Invalid note"),
|
||||||
ErrorKind::InvalidWitnessAnchor(id_note, last_height) => write!(
|
ErrorKind::InvalidWitnessAnchor(id_note, last_height) => write!(
|
||||||
f,
|
f,
|
||||||
"Witness for note {} has incorrect anchor after scanning block {}",
|
"Witness for note {} has incorrect anchor after scanning block {}",
|
||||||
@ -43,6 +61,7 @@ impl fmt::Display for Error {
|
|||||||
ErrorKind::ScanRequired => write!(f, "Must scan blocks first"),
|
ErrorKind::ScanRequired => write!(f, "Must scan blocks first"),
|
||||||
ErrorKind::TableNotEmpty => write!(f, "Table is not empty"),
|
ErrorKind::TableNotEmpty => write!(f, "Table is not empty"),
|
||||||
ErrorKind::Bech32(e) => write!(f, "{}", e),
|
ErrorKind::Bech32(e) => write!(f, "{}", e),
|
||||||
|
ErrorKind::Builder(e) => write!(f, "{:?}", e),
|
||||||
ErrorKind::Database(e) => write!(f, "{}", e),
|
ErrorKind::Database(e) => write!(f, "{}", e),
|
||||||
ErrorKind::Io(e) => write!(f, "{}", e),
|
ErrorKind::Io(e) => write!(f, "{}", e),
|
||||||
ErrorKind::Protobuf(e) => write!(f, "{}", e),
|
ErrorKind::Protobuf(e) => write!(f, "{}", e),
|
||||||
@ -53,7 +72,9 @@ impl fmt::Display for Error {
|
|||||||
impl error::Error for Error {
|
impl error::Error for Error {
|
||||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
|
ErrorKind::InvalidMemo(e) => Some(e),
|
||||||
ErrorKind::Bech32(e) => Some(e),
|
ErrorKind::Bech32(e) => Some(e),
|
||||||
|
ErrorKind::Builder(e) => Some(e),
|
||||||
ErrorKind::Database(e) => Some(e),
|
ErrorKind::Database(e) => Some(e),
|
||||||
ErrorKind::Io(e) => Some(e),
|
ErrorKind::Io(e) => Some(e),
|
||||||
ErrorKind::Protobuf(e) => Some(e),
|
ErrorKind::Protobuf(e) => Some(e),
|
||||||
@ -68,6 +89,12 @@ impl From<bech32::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<builder::Error> for Error {
|
||||||
|
fn from(e: builder::Error) -> Self {
|
||||||
|
Error(ErrorKind::Builder(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<rusqlite::Error> for Error {
|
impl From<rusqlite::Error> for Error {
|
||||||
fn from(e: rusqlite::Error) -> Self {
|
fn from(e: rusqlite::Error) -> Self {
|
||||||
Error(ErrorKind::Database(e))
|
Error(ErrorKind::Database(e))
|
||||||
|
@ -30,6 +30,7 @@ pub mod error;
|
|||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod scan;
|
pub mod scan;
|
||||||
|
pub mod transact;
|
||||||
|
|
||||||
const ANCHOR_OFFSET: u32 = 10;
|
const ANCHOR_OFFSET: u32 = 10;
|
||||||
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000;
|
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000;
|
||||||
|
661
zcash_client_sqlite/src/transact.rs
Normal file
661
zcash_client_sqlite/src/transact.rs
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
//! Functions for creating transactions.
|
||||||
|
|
||||||
|
use ff::{PrimeField, PrimeFieldRepr};
|
||||||
|
use pairing::bls12_381::Bls12;
|
||||||
|
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
|
||||||
|
use std::path::Path;
|
||||||
|
use zcash_client_backend::{
|
||||||
|
constants::testnet::{HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_PAYMENT_ADDRESS},
|
||||||
|
encoding::{encode_extended_full_viewing_key, encode_payment_address},
|
||||||
|
};
|
||||||
|
use zcash_primitives::{
|
||||||
|
jubjub::fs::{Fs, FsRepr},
|
||||||
|
merkle_tree::IncrementalWitness,
|
||||||
|
note_encryption::Memo,
|
||||||
|
primitives::{Diversifier, Note, PaymentAddress},
|
||||||
|
prover::TxProver,
|
||||||
|
sapling::Node,
|
||||||
|
transaction::{
|
||||||
|
builder::Builder,
|
||||||
|
components::{amount::DEFAULT_FEE, Amount},
|
||||||
|
},
|
||||||
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||||
|
JUBJUB,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
get_target_and_anchor_heights,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SelectedNoteRow {
|
||||||
|
diversifier: Diversifier,
|
||||||
|
note: Note<Bls12>,
|
||||||
|
witness: IncrementalWitness<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a transaction paying the specified address from the given account.
|
||||||
|
///
|
||||||
|
/// Returns the row index of the newly-created transaction in the `transactions` table
|
||||||
|
/// within the data database. The caller can read the raw transaction bytes from the `raw`
|
||||||
|
/// column in order to broadcast the transaction to the network.
|
||||||
|
///
|
||||||
|
/// Do not call this multiple times in parallel, or you will generate transactions that
|
||||||
|
/// double-spend the same notes.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use zcash_client_backend::{
|
||||||
|
/// constants::{testnet::COIN_TYPE, SAPLING_CONSENSUS_BRANCH_ID},
|
||||||
|
/// keys::spending_key,
|
||||||
|
/// };
|
||||||
|
/// use zcash_client_sqlite::transact::create_to_address;
|
||||||
|
/// use zcash_primitives::transaction::components::Amount;
|
||||||
|
/// use zcash_proofs::prover::LocalTxProver;
|
||||||
|
///
|
||||||
|
/// let tx_prover = match LocalTxProver::with_default_location() {
|
||||||
|
/// Some(tx_prover) => tx_prover,
|
||||||
|
/// None => {
|
||||||
|
/// panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
|
||||||
|
/// }
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let account = 0;
|
||||||
|
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account);
|
||||||
|
/// let to = extsk.default_address().unwrap().1;
|
||||||
|
/// match create_to_address(
|
||||||
|
/// "/path/to/data.db",
|
||||||
|
/// SAPLING_CONSENSUS_BRANCH_ID,
|
||||||
|
/// tx_prover,
|
||||||
|
/// (account, &extsk),
|
||||||
|
/// &to,
|
||||||
|
/// Amount::from_u64(1).unwrap(),
|
||||||
|
/// None,
|
||||||
|
/// ) {
|
||||||
|
/// Ok(tx_row) => (),
|
||||||
|
/// Err(e) => (),
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn create_to_address<P: AsRef<Path>>(
|
||||||
|
db_data: P,
|
||||||
|
consensus_branch_id: u32,
|
||||||
|
prover: impl TxProver,
|
||||||
|
(account, extsk): (u32, &ExtendedSpendingKey),
|
||||||
|
to: &PaymentAddress<Bls12>,
|
||||||
|
value: Amount,
|
||||||
|
memo: Option<Memo>,
|
||||||
|
) -> Result<i64, Error> {
|
||||||
|
let data = Connection::open(db_data)?;
|
||||||
|
|
||||||
|
// Check that the ExtendedSpendingKey we have been given corresponds to the
|
||||||
|
// ExtendedFullViewingKey for the account we are spending from.
|
||||||
|
let extfvk = ExtendedFullViewingKey::from(extsk);
|
||||||
|
if !data
|
||||||
|
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
|
||||||
|
.exists(&[
|
||||||
|
account.to_sql()?,
|
||||||
|
encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
|
||||||
|
.to_sql()?,
|
||||||
|
])?
|
||||||
|
{
|
||||||
|
return Err(Error(ErrorKind::InvalidExtSK(account)));
|
||||||
|
}
|
||||||
|
let ovk = extfvk.fvk.ovk;
|
||||||
|
|
||||||
|
// Target the next block, assuming we are up-to-date.
|
||||||
|
let (height, anchor_height) = {
|
||||||
|
let (target_height, anchor_height) = get_target_and_anchor_heights(&data)?;
|
||||||
|
(target_height, i64::from(anchor_height))
|
||||||
|
};
|
||||||
|
|
||||||
|
// The goal of this SQL statement is to select the oldest notes until the required
|
||||||
|
// value has been reached, and then fetch the witnesses at the desired height for the
|
||||||
|
// selected notes. This is achieved in several steps:
|
||||||
|
//
|
||||||
|
// 1) Use a window function to create a view of all notes, ordered from oldest to
|
||||||
|
// newest, with an additional column containing a running sum:
|
||||||
|
// - Unspent notes accumulate the values of all unspent notes in that note's
|
||||||
|
// account, up to itself.
|
||||||
|
// - Spent notes accumulate the values of all notes in the transaction they were
|
||||||
|
// spent in, up to itself.
|
||||||
|
//
|
||||||
|
// 2) Select all unspent notes in the desired account, along with their running sum.
|
||||||
|
//
|
||||||
|
// 3) Select all notes for which the running sum was less than the required value, as
|
||||||
|
// well as a single note for which the sum was greater than or equal to the
|
||||||
|
// required value, bringing the sum of all selected notes across the threshold.
|
||||||
|
//
|
||||||
|
// 4) Match the selected notes against the witnesses at the desired height.
|
||||||
|
let target_value = i64::from(value + DEFAULT_FEE);
|
||||||
|
let mut stmt_select_notes = data.prepare(
|
||||||
|
"WITH selected AS (
|
||||||
|
WITH eligible AS (
|
||||||
|
SELECT id_note, diversifier, value, rcm,
|
||||||
|
SUM(value) OVER
|
||||||
|
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
||||||
|
FROM received_notes
|
||||||
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||||
|
WHERE account = ? AND spent IS NULL AND transactions.block <= ?
|
||||||
|
)
|
||||||
|
SELECT * FROM eligible WHERE so_far < ?
|
||||||
|
UNION
|
||||||
|
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= ? LIMIT 1)
|
||||||
|
), witnesses AS (
|
||||||
|
SELECT note, witness FROM sapling_witnesses
|
||||||
|
WHERE block = ?
|
||||||
|
)
|
||||||
|
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
||||||
|
FROM selected
|
||||||
|
INNER JOIN witnesses ON selected.id_note = witnesses.note",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Select notes
|
||||||
|
let notes = stmt_select_notes.query_and_then::<_, Error, _, _>(
|
||||||
|
&[
|
||||||
|
i64::from(account),
|
||||||
|
anchor_height,
|
||||||
|
target_value,
|
||||||
|
target_value,
|
||||||
|
anchor_height,
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
let diversifier = {
|
||||||
|
let d: Vec<_> = row.get(0)?;
|
||||||
|
if d.len() != 11 {
|
||||||
|
return Err(Error(ErrorKind::CorruptedData(
|
||||||
|
"Invalid diversifier length",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut tmp = [0; 11];
|
||||||
|
tmp.copy_from_slice(&d);
|
||||||
|
Diversifier(tmp)
|
||||||
|
};
|
||||||
|
|
||||||
|
let note_value: i64 = row.get(1)?;
|
||||||
|
|
||||||
|
let rcm = {
|
||||||
|
let d: Vec<_> = row.get(2)?;
|
||||||
|
let mut tmp = FsRepr::default();
|
||||||
|
tmp.read_le(&d[..])?;
|
||||||
|
Fs::from_repr(tmp).map_err(|_| Error(ErrorKind::InvalidNote))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let from = extfvk
|
||||||
|
.fvk
|
||||||
|
.vk
|
||||||
|
.into_payment_address(diversifier, &JUBJUB)
|
||||||
|
.unwrap();
|
||||||
|
let note = from.create_note(note_value as u64, rcm, &JUBJUB).unwrap();
|
||||||
|
|
||||||
|
let witness = {
|
||||||
|
let d: Vec<_> = row.get(3)?;
|
||||||
|
IncrementalWitness::read(&d[..])?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SelectedNoteRow {
|
||||||
|
diversifier,
|
||||||
|
note,
|
||||||
|
witness,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let notes: Vec<SelectedNoteRow> = notes.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
// Confirm we were able to select sufficient value
|
||||||
|
let selected_value = notes
|
||||||
|
.iter()
|
||||||
|
.fold(0, |acc, selected| acc + selected.note.value);
|
||||||
|
if selected_value < target_value as u64 {
|
||||||
|
return Err(Error(ErrorKind::InsufficientBalance(
|
||||||
|
selected_value,
|
||||||
|
target_value as u64,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the transaction
|
||||||
|
let mut builder = Builder::new(height);
|
||||||
|
for selected in notes {
|
||||||
|
builder.add_sapling_spend(
|
||||||
|
extsk.clone(),
|
||||||
|
selected.diversifier,
|
||||||
|
selected.note,
|
||||||
|
selected.witness,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
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 = 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();
|
||||||
|
|
||||||
|
// Update the database atomically, to ensure the result is internally consistent.
|
||||||
|
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||||
|
|
||||||
|
// 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, created, expiry_height, raw)
|
||||||
|
VALUES (?, ?, ?, ?)",
|
||||||
|
)?;
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Mark notes as spent.
|
||||||
|
//
|
||||||
|
// This locks the notes so they aren't selected again by a subsequent call to
|
||||||
|
// create_to_address() before this transaction has been mined (at which point the notes
|
||||||
|
// get re-marked as spent).
|
||||||
|
//
|
||||||
|
// Assumes that create_to_address() will never be called in parallel, which is a
|
||||||
|
// reasonable assumption for a light client such as a mobile phone.
|
||||||
|
let mut stmt_mark_spent_note =
|
||||||
|
data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||||
|
for spend in &tx.shielded_spends {
|
||||||
|
stmt_mark_spent_note.execute(&[id_tx.to_sql()?, spend.nullifier.to_sql()?])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the sent note in the database.
|
||||||
|
let to_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, to);
|
||||||
|
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 (?, ?, ?, ?, ?, ?)",
|
||||||
|
)?;
|
||||||
|
stmt_insert_sent_note.execute(&[
|
||||||
|
id_tx.to_sql()?,
|
||||||
|
output_index.to_sql()?,
|
||||||
|
account.to_sql()?,
|
||||||
|
to_str.to_sql()?,
|
||||||
|
i64::from(value).to_sql()?,
|
||||||
|
memo.as_bytes().to_sql()?,
|
||||||
|
])?;
|
||||||
|
} else {
|
||||||
|
let mut stmt_insert_sent_note = data.prepare(
|
||||||
|
"INSERT INTO sent_notes (tx, output_index, from_account, address, value)
|
||||||
|
VALUES (?, ?, ?, ?, ?)",
|
||||||
|
)?;
|
||||||
|
stmt_insert_sent_note.execute(&[
|
||||||
|
id_tx.to_sql()?,
|
||||||
|
output_index.to_sql()?,
|
||||||
|
account.to_sql()?,
|
||||||
|
to_str.to_sql()?,
|
||||||
|
i64::from(value).to_sql()?,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.execute("COMMIT", NO_PARAMS)?;
|
||||||
|
|
||||||
|
// Return the row number of the transaction, so the caller can fetch it for sending.
|
||||||
|
Ok(id_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use zcash_primitives::{
|
||||||
|
block::BlockHash,
|
||||||
|
prover::TxProver,
|
||||||
|
transaction::components::Amount,
|
||||||
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||||
|
};
|
||||||
|
use zcash_proofs::prover::LocalTxProver;
|
||||||
|
|
||||||
|
use super::create_to_address;
|
||||||
|
use crate::{
|
||||||
|
init::{init_accounts_table, init_blocks_table, init_cache_database, init_data_database},
|
||||||
|
query::{get_balance, get_verified_balance},
|
||||||
|
scan::scan_cached_blocks,
|
||||||
|
tests::{fake_compact_block, insert_into_cache},
|
||||||
|
SAPLING_ACTIVATION_HEIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn test_prover() -> impl TxProver {
|
||||||
|
match LocalTxProver::with_default_location() {
|
||||||
|
Some(tx_prover) => tx_prover,
|
||||||
|
None => {
|
||||||
|
panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_to_address_fails_on_incorrect_extsk() {
|
||||||
|
let data_file = NamedTempFile::new().unwrap();
|
||||||
|
let db_data = data_file.path();
|
||||||
|
init_data_database(&db_data).unwrap();
|
||||||
|
|
||||||
|
// Add two accounts to the wallet
|
||||||
|
let extsk0 = ExtendedSpendingKey::master(&[]);
|
||||||
|
let extsk1 = ExtendedSpendingKey::master(&[0]);
|
||||||
|
let extfvks = [
|
||||||
|
ExtendedFullViewingKey::from(&extsk0),
|
||||||
|
ExtendedFullViewingKey::from(&extsk1),
|
||||||
|
];
|
||||||
|
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||||
|
let to = extsk0.default_address().unwrap().1;
|
||||||
|
|
||||||
|
// Invalid extsk for the given account should cause an error
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk1),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(1).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
|
||||||
|
}
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(1, &extsk0),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(1).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_to_address_fails_with_no_blocks() {
|
||||||
|
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();
|
||||||
|
let to = extsk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
// We cannot do anything if we aren't synchronised
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(1).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_to_address_fails_on_insufficient_balance() {
|
||||||
|
let data_file = NamedTempFile::new().unwrap();
|
||||||
|
let db_data = data_file.path();
|
||||||
|
init_data_database(&db_data).unwrap();
|
||||||
|
init_blocks_table(&db_data, 1, BlockHash([1; 32]), 1, &[]).unwrap();
|
||||||
|
|
||||||
|
// Add an account to the wallet
|
||||||
|
let extsk = ExtendedSpendingKey::master(&[]);
|
||||||
|
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
||||||
|
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||||
|
let to = extsk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
// Account balance should be zero
|
||||||
|
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||||
|
|
||||||
|
// We cannot spend anything
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(1).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(
|
||||||
|
e.to_string(),
|
||||||
|
"Insufficient balance (have 0, need 10001 including fee)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_to_address_fails_on_unverified_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();
|
||||||
|
|
||||||
|
// Add funds to the wallet in a single note
|
||||||
|
let value = Amount::from_u64(50000).unwrap();
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT,
|
||||||
|
BlockHash([0; 32]),
|
||||||
|
extfvk.clone(),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
|
||||||
|
// Verified balance matches total balance
|
||||||
|
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||||
|
assert_eq!(get_verified_balance(db_data, 0).unwrap(), value);
|
||||||
|
|
||||||
|
// Add more funds to the wallet in a second note
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||||
|
cb.hash(),
|
||||||
|
extfvk.clone(),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
|
||||||
|
// Verified balance does not include the second note
|
||||||
|
assert_eq!(get_balance(db_data, 0).unwrap(), value + value);
|
||||||
|
assert_eq!(get_verified_balance(db_data, 0).unwrap(), value);
|
||||||
|
|
||||||
|
// Spend fails because there are insufficient verified notes
|
||||||
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||||
|
let to = extsk2.default_address().unwrap().1;
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(70000).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(
|
||||||
|
e.to_string(),
|
||||||
|
"Insufficient balance (have 50000, need 80000 including fee)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
|
||||||
|
// note is verified
|
||||||
|
for i in 2..10 {
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT + i,
|
||||||
|
cb.hash(),
|
||||||
|
extfvk.clone(),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
}
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
|
||||||
|
// Second spend still fails
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(70000).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(
|
||||||
|
e.to_string(),
|
||||||
|
"Insufficient balance (have 50000, need 80000 including fee)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine block 11 so that the second note becomes verified
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT + 10,
|
||||||
|
cb.hash(),
|
||||||
|
extfvk.clone(),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
|
||||||
|
// Second spend should now succeed
|
||||||
|
create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(70000).unwrap(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_to_address_fails_on_locked_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();
|
||||||
|
|
||||||
|
// Add funds to the wallet in a single note
|
||||||
|
let value = Amount::from_u64(50000).unwrap();
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT,
|
||||||
|
BlockHash([0; 32]),
|
||||||
|
extfvk.clone(),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||||
|
|
||||||
|
// Send some of the funds to another address
|
||||||
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||||
|
let to = extsk2.default_address().unwrap().1;
|
||||||
|
create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(15000).unwrap(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// A second spend fails because there are no usable notes
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(2000).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(
|
||||||
|
e.to_string(),
|
||||||
|
"Insufficient balance (have 0, need 12000 including fee)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
|
||||||
|
// until just before the first transaction expires
|
||||||
|
for i in 1..22 {
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT + i,
|
||||||
|
cb.hash(),
|
||||||
|
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
}
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
|
||||||
|
// Second spend still fails
|
||||||
|
match create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(2000).unwrap(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("Should have failed"),
|
||||||
|
Err(e) => assert_eq!(
|
||||||
|
e.to_string(),
|
||||||
|
"Insufficient balance (have 0, need 12000 including fee)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
SAPLING_ACTIVATION_HEIGHT + 22,
|
||||||
|
cb.hash(),
|
||||||
|
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
insert_into_cache(db_cache, &cb);
|
||||||
|
scan_cached_blocks(db_cache, db_data).unwrap();
|
||||||
|
|
||||||
|
// Second spend should now succeed
|
||||||
|
create_to_address(
|
||||||
|
db_data,
|
||||||
|
1,
|
||||||
|
test_prover(),
|
||||||
|
(0, &extsk),
|
||||||
|
&to,
|
||||||
|
Amount::from_u64(2000).unwrap(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,8 @@ use crate::{
|
|||||||
use ff::Field;
|
use ff::Field;
|
||||||
use pairing::bls12_381::{Bls12, Fr};
|
use pairing::bls12_381::{Bls12, Fr};
|
||||||
use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore};
|
use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore};
|
||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
use zip32::ExtendedSpendingKey;
|
use zip32::ExtendedSpendingKey;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -42,6 +44,27 @@ pub enum Error {
|
|||||||
SpendProof,
|
SpendProof,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::AnchorMismatch => {
|
||||||
|
write!(f, "Anchor mismatch (anchors for all spends must be equal)")
|
||||||
|
}
|
||||||
|
Error::BindingSig => write!(f, "Failed to create bindingSig"),
|
||||||
|
Error::ChangeIsNegative(amount) => {
|
||||||
|
write!(f, "Change is negative ({:?} zatoshis)", amount)
|
||||||
|
}
|
||||||
|
Error::InvalidAddress => write!(f, "Invalid address"),
|
||||||
|
Error::InvalidAmount => write!(f, "Invalid amount"),
|
||||||
|
Error::InvalidWitness => write!(f, "Invalid note witness"),
|
||||||
|
Error::NoChangeAddress => write!(f, "No change address specified or discoverable"),
|
||||||
|
Error::SpendProof => write!(f, "Failed to create Sapling spend proof"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {}
|
||||||
|
|
||||||
struct SpendDescriptionInfo {
|
struct SpendDescriptionInfo {
|
||||||
extsk: ExtendedSpendingKey,
|
extsk: ExtendedSpendingKey,
|
||||||
diversifier: Diversifier,
|
diversifier: Diversifier,
|
||||||
|
Loading…
Reference in New Issue
Block a user