2019-03-08 19:20:32 -08:00
//! Functions for creating transactions.
use ff ::PrimeField ;
2020-08-05 18:14:45 -07:00
use rusqlite ::{ types ::ToSql , NO_PARAMS } ;
2019-03-08 19:20:32 -08:00
use std ::convert ::TryInto ;
2020-08-05 18:14:45 -07:00
use zcash_client_backend ::{
address ::RecipientAddress , data_api ::error ::Error , encoding ::encode_extended_full_viewing_key ,
} ;
2019-03-08 19:20:32 -08:00
use zcash_primitives ::{
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
consensus ,
2020-07-09 04:48:09 -07:00
keys ::OutgoingViewingKey ,
2019-03-08 19:20:32 -08:00
merkle_tree ::{ IncrementalWitness , MerklePath } ,
note_encryption ::Memo ,
2020-07-29 21:36:12 -07:00
primitives ::{ Diversifier , Note , Rseed } ,
2019-03-08 19:20:32 -08:00
prover ::TxProver ,
sapling ::Node ,
transaction ::{
builder ::Builder ,
components ::{ amount ::DEFAULT_FEE , Amount } ,
} ,
zip32 ::{ ExtendedFullViewingKey , ExtendedSpendingKey } ,
} ;
2020-08-05 18:14:45 -07:00
use crate ::{ error ::SqliteClientError , get_target_and_anchor_heights , DataConnection } ;
2019-03-08 19:20:32 -08:00
2020-07-09 04:48:09 -07:00
/// Describes a policy for which outgoing viewing key should be able to decrypt
/// transaction outputs.
///
/// For details on what transaction information is visible to the holder of an outgoing
/// viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
pub enum OvkPolicy {
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
///
/// Transaction outputs will be decryptable by the sender, in addition to the
/// recipients.
Sender ,
/// Use a custom outgoing viewing key. This might for instance be derived from a
/// separate seed than the wallet's spending keys.
///
/// Transaction outputs will be decryptable by the recipients, and whoever controls
/// the provided outgoing viewing key.
Custom ( OutgoingViewingKey ) ,
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
/// recipients, but not by the sender.
Discard ,
}
2019-03-08 19:20:32 -08:00
struct SelectedNoteRow {
diversifier : Diversifier ,
2020-07-01 13:26:54 -07:00
note : Note ,
2019-03-08 19:20:32 -08:00
merkle_path : MerklePath < 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.
///
2020-07-09 04:48:09 -07:00
/// # Transaction privacy
///
/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be
/// able to decrypt the outputs of this transaction. This is primarily relevant to
/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the
/// recipient's address, and the contents of `memo`, from ever being recovered from the
/// block chain. (The total value sent can always be inferred by the sender from the spent
/// notes and received change.)
///
/// Regardless of the specified policy, `create_to_address` saves `to`, `value`, and
/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`.
///
/// For details on what transaction information is visible to the holder of a full or
/// outgoing viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
///
2019-03-08 19:20:32 -08:00
/// # Examples
///
/// ```
2020-08-05 18:14:45 -07:00
/// use tempfile::NamedTempFile;
2020-08-05 13:27:40 -07:00
/// use zcash_primitives::{
/// consensus::{self, Network},
2019-03-08 19:20:32 -08:00
/// constants::testnet::COIN_TYPE,
2020-08-05 13:27:40 -07:00
/// transaction::components::Amount
/// };
/// use zcash_proofs::prover::LocalTxProver;
/// use zcash_client_backend::{
2019-03-08 19:20:32 -08:00
/// keys::spending_key,
/// };
2020-08-05 18:14:45 -07:00
/// use zcash_client_sqlite::{
/// DataConnection,
/// transact::{create_to_address, OvkPolicy},
/// };
2019-03-08 19:20:32 -08:00
///
/// 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);
2019-05-24 07:59:18 -07:00
/// let to = extsk.default_address().unwrap().1.into();
2020-08-05 18:14:45 -07:00
///
/// let data_file = NamedTempFile::new().unwrap();
/// let db = DataConnection::for_path(data_file).unwrap();
2019-03-08 19:20:32 -08:00
/// match create_to_address(
2020-08-05 18:14:45 -07:00
/// &db,
2020-08-05 13:27:40 -07:00
/// &Network::TestNetwork,
2019-03-08 19:20:32 -08:00
/// consensus::BranchId::Sapling,
/// tx_prover,
/// (account, &extsk),
/// &to,
/// Amount::from_u64(1).unwrap(),
/// None,
2020-07-09 04:48:09 -07:00
/// OvkPolicy::Sender,
2019-03-08 19:20:32 -08:00
/// ) {
/// Ok(tx_row) => (),
/// Err(e) => (),
/// }
/// ```
2020-08-05 18:14:45 -07:00
pub fn create_to_address < P : consensus ::Parameters > (
data : & DataConnection ,
2020-08-05 13:27:40 -07:00
params : & P ,
2019-03-08 19:20:32 -08:00
consensus_branch_id : consensus ::BranchId ,
prover : impl TxProver ,
( account , extsk ) : ( u32 , & ExtendedSpendingKey ) ,
2019-05-24 07:59:18 -07:00
to : & RecipientAddress ,
2019-03-08 19:20:32 -08:00
value : Amount ,
memo : Option < Memo > ,
2020-07-09 04:48:09 -07:00
ovk_policy : OvkPolicy ,
2020-08-05 18:14:45 -07:00
) -> Result < i64 , SqliteClientError > {
2019-03-08 19:20:32 -08:00
// 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
2020-08-05 18:14:45 -07:00
. 0
2019-03-08 19:20:32 -08:00
. prepare ( " SELECT * FROM accounts WHERE account = ? AND extfvk = ? " ) ?
. exists ( & [
account . to_sql ( ) ? ,
2020-08-05 13:27:40 -07:00
encode_extended_full_viewing_key (
params . hrp_sapling_extended_full_viewing_key ( ) ,
& extfvk ,
)
. to_sql ( ) ? ,
2019-03-08 19:20:32 -08:00
] ) ?
{
2020-08-05 18:14:45 -07:00
return Err ( Error ::InvalidExtSK ( account ) . into ( ) ) ;
2019-03-08 19:20:32 -08:00
}
2020-07-09 04:48:09 -07:00
// Apply the outgoing viewing key policy.
let ovk = match ovk_policy {
2020-08-28 08:12:37 -07:00
OvkPolicy ::Sender = > Some ( extfvk . fvk . ovk ) ,
OvkPolicy ::Custom ( ovk ) = > Some ( ovk ) ,
OvkPolicy ::Discard = > None ,
2020-07-09 04:48:09 -07:00
} ;
2019-03-08 19:20:32 -08:00
// Target the next block, assuming we are up-to-date.
2020-08-05 13:08:58 -07:00
let ( height , anchor_height ) = get_target_and_anchor_heights ( & data ) ? ;
2019-03-08 19:20:32 -08:00
// 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 ) ;
2020-08-05 18:14:45 -07:00
let mut stmt_select_notes = data . 0. prepare (
2019-03-08 19:20:32 -08:00
" 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
2020-08-05 18:14:45 -07:00
let notes = stmt_select_notes . query_and_then ::< _ , SqliteClientError , _ , _ > (
2019-03-08 19:20:32 -08:00
& [
i64 ::from ( account ) ,
2020-08-05 13:08:58 -07:00
i64 ::from ( anchor_height ) ,
2019-03-08 19:20:32 -08:00
target_value ,
target_value ,
2020-08-05 13:08:58 -07:00
i64 ::from ( anchor_height ) ,
2019-03-08 19:20:32 -08:00
] ,
| row | {
let diversifier = {
let d : Vec < _ > = row . get ( 0 ) ? ;
if d . len ( ) ! = 11 {
2020-08-05 18:14:45 -07:00
return Err ( SqliteClientError ( Error ::CorruptedData (
2019-03-08 19:20:32 -08:00
" Invalid diversifier length " ,
) ) ) ;
}
let mut tmp = [ 0 ; 11 ] ;
tmp . copy_from_slice ( & d ) ;
Diversifier ( tmp )
} ;
let note_value : i64 = row . get ( 1 ) ? ;
2020-08-05 21:00:49 -07:00
let rseed = {
2020-10-24 04:05:15 -07:00
let rcm_bytes : Vec < _ > = row . get ( 2 ) ? ;
2020-08-04 23:26:57 -07:00
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
// We store rcm directly in the data DB, regardless of whether the note
// used a v1 or v2 note plaintext, so for the purposes of spending let's
// pretend this is a pre-ZIP 212 note.
2020-10-24 04:05:15 -07:00
let rcm = jubjub ::Fr ::from_repr (
rcm_bytes [ .. ]
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
. try_into ( )
2020-08-05 18:14:45 -07:00
. map_err ( | _ | SqliteClientError ( Error ::InvalidNote ) ) ? ,
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
)
2020-08-05 18:14:45 -07:00
. ok_or ( SqliteClientError ( Error ::InvalidNote ) ) ? ;
2020-10-24 04:05:15 -07:00
Rseed ::BeforeZip212 ( rcm )
2019-03-08 19:20:32 -08:00
} ;
2020-07-01 13:26:54 -07:00
let from = extfvk . fvk . vk . to_payment_address ( diversifier ) . unwrap ( ) ;
let note = from . create_note ( note_value as u64 , rseed ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
let merkle_path = {
let d : Vec < _ > = row . get ( 3 ) ? ;
IncrementalWitness ::read ( & d [ .. ] ) ?
. path ( )
. expect ( " the tree is not empty " )
} ;
Ok ( SelectedNoteRow {
diversifier ,
note ,
merkle_path ,
} )
} ,
) ? ;
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 {
2020-08-05 18:14:45 -07:00
return Err ( Error ::InsufficientBalance ( selected_value , target_value as u64 ) . into ( ) ) ;
2019-03-08 19:20:32 -08:00
}
// Create the transaction
2020-08-05 13:27:40 -07:00
let mut builder = Builder ::new ( params . clone ( ) , height ) ;
2019-03-08 19:20:32 -08:00
for selected in notes {
builder . add_sapling_spend (
extsk . clone ( ) ,
selected . diversifier ,
selected . note ,
selected . merkle_path ,
) ? ;
}
2019-05-24 07:59:18 -07:00
match to {
RecipientAddress ::Shielded ( to ) = > {
2020-08-05 20:36:02 -07:00
builder . add_sapling_output ( ovk , to . clone ( ) , value , memo . clone ( ) )
2019-05-24 07:59:18 -07:00
}
RecipientAddress ::Transparent ( to ) = > builder . add_transparent_output ( & to , value ) ,
} ? ;
2019-03-08 19:20:32 -08:00
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 " ) ,
} ;
2020-09-08 16:53:42 -07:00
let created = time ::OffsetDateTime ::now_utc ( ) ;
2019-03-08 19:20:32 -08:00
// Update the database atomically, to ensure the result is internally consistent.
2020-08-05 18:14:45 -07:00
data . 0. execute ( " BEGIN IMMEDIATE " , NO_PARAMS ) ? ;
2019-03-08 19:20:32 -08:00
// Save the transaction in the database.
let mut raw_tx = vec! [ ] ;
tx . write ( & mut raw_tx ) ? ;
2020-08-05 18:14:45 -07:00
let mut stmt_insert_tx = data . 0. prepare (
2019-03-08 19:20:32 -08:00
" INSERT INTO transactions (txid, created, expiry_height, raw)
VALUES ( ? , ? , ? , ? ) " ,
) ? ;
stmt_insert_tx . execute ( & [
tx . txid ( ) . 0. to_sql ( ) ? ,
created . to_sql ( ) ? ,
2020-08-05 13:08:58 -07:00
i64 ::from ( tx . expiry_height ) . to_sql ( ) ? ,
2019-03-08 19:20:32 -08:00
raw_tx . to_sql ( ) ? ,
] ) ? ;
2020-08-05 18:14:45 -07:00
let id_tx = data . 0. last_insert_rowid ( ) ;
2019-03-08 19:20:32 -08:00
// 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.
2020-08-05 18:14:45 -07:00
let mut stmt_mark_spent_note = data
. 0
. prepare ( " UPDATE received_notes SET spent = ? WHERE nf = ? " ) ? ;
2019-03-08 19:20:32 -08:00
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.
2019-05-24 07:59:18 -07:00
// TODO: Decide how to save transparent output information.
2020-09-18 09:40:30 -07:00
let to_str = to . encode ( params ) ;
2019-03-08 19:20:32 -08:00
if let Some ( memo ) = memo {
2020-08-05 18:14:45 -07:00
let mut stmt_insert_sent_note = data . 0. prepare (
2019-03-08 19:20:32 -08:00
" 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 {
2020-08-05 18:14:45 -07:00
let mut stmt_insert_sent_note = data . 0. prepare (
2019-03-08 19:20:32 -08:00
" 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 ( ) ? ,
] ) ? ;
}
2020-08-05 18:14:45 -07:00
data . 0. execute ( " COMMIT " , NO_PARAMS ) ? ;
2019-03-08 19:20:32 -08:00
// Return the row number of the transaction, so the caller can fetch it for sending.
Ok ( id_tx )
}
#[ cfg(test) ]
mod tests {
2020-07-09 04:48:09 -07:00
use rusqlite ::Connection ;
2019-03-08 19:20:32 -08:00
use tempfile ::NamedTempFile ;
2020-08-05 13:27:40 -07:00
2019-03-08 19:20:32 -08:00
use zcash_primitives ::{
block ::BlockHash ,
consensus ,
2020-07-09 04:48:09 -07:00
note_encryption ::try_sapling_output_recovery ,
2019-03-08 19:20:32 -08:00
prover ::TxProver ,
2020-07-09 04:48:09 -07:00
transaction ::{ components ::Amount , Transaction } ,
2019-03-08 19:20:32 -08:00
zip32 ::{ ExtendedFullViewingKey , ExtendedSpendingKey } ,
} ;
2020-08-05 13:27:40 -07:00
2019-03-08 19:20:32 -08:00
use zcash_proofs ::prover ::LocalTxProver ;
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 ,
2020-08-05 13:27:40 -07:00
tests ::{ self , fake_compact_block , insert_into_cache , sapling_activation_height } ,
2020-08-05 18:14:45 -07:00
CacheConnection , DataConnection ,
2019-03-08 19:20:32 -08:00
} ;
2020-08-05 13:27:40 -07:00
use super ::{ create_to_address , OvkPolicy } ;
2019-03-08 19:20:32 -08:00
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 ( ) ;
2020-08-05 18:14:45 -07:00
let db_data = DataConnection ( Connection ::open ( data_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
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 ) ,
] ;
2020-08-05 13:27:40 -07:00
init_accounts_table ( & db_data , & tests ::network ( ) , & extfvks ) . unwrap ( ) ;
2019-05-24 07:59:18 -07:00
let to = extsk0 . default_address ( ) . unwrap ( ) . 1. into ( ) ;
2019-03-08 19:20:32 -08:00
// Invalid extsk for the given account should cause an error
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk1 ) ,
& to ,
Amount ::from_u64 ( 1 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! ( e . to_string ( ) , " Incorrect ExtendedSpendingKey for account 0 " ) ,
}
2020-08-05 18:14:45 -07:00
2019-03-08 19:20:32 -08:00
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 1 , & extsk0 ) ,
& to ,
Amount ::from_u64 ( 1 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
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 ( ) ;
2020-08-05 18:14:45 -07:00
let db_data = DataConnection ( Connection ::open ( data_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
init_data_database ( & db_data ) . unwrap ( ) ;
// Add an account to the wallet
let extsk = ExtendedSpendingKey ::master ( & [ ] ) ;
let extfvks = [ ExtendedFullViewingKey ::from ( & extsk ) ] ;
2020-08-05 13:27:40 -07:00
init_accounts_table ( & db_data , & tests ::network ( ) , & extfvks ) . unwrap ( ) ;
2019-05-24 07:59:18 -07:00
let to = extsk . default_address ( ) . unwrap ( ) . 1. into ( ) ;
2019-03-08 19:20:32 -08:00
// We cannot do anything if we aren't synchronised
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 1 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
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 ( ) ;
2020-08-05 18:14:45 -07:00
let db_data = DataConnection ( Connection ::open ( data_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
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 ) ] ;
2020-08-05 13:27:40 -07:00
init_accounts_table ( & db_data , & tests ::network ( ) , & extfvks ) . unwrap ( ) ;
2019-05-24 07:59:18 -07:00
let to = extsk . default_address ( ) . unwrap ( ) . 1. into ( ) ;
2019-03-08 19:20:32 -08:00
// Account balance should be zero
2020-08-05 18:14:45 -07:00
assert_eq! ( get_balance ( & db_data , 0 ) . unwrap ( ) , Amount ::zero ( ) ) ;
2019-03-08 19:20:32 -08:00
// We cannot spend anything
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 1 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! (
e . to_string ( ) ,
2020-11-23 17:50:47 -08:00
" Insufficient balance (have 0, need 1001 including fee) "
2019-03-08 19:20:32 -08:00
) ,
}
}
#[ test ]
fn create_to_address_fails_on_unverified_notes ( ) {
let cache_file = NamedTempFile ::new ( ) . unwrap ( ) ;
2020-08-05 18:14:45 -07:00
let db_cache = CacheConnection ( Connection ::open ( cache_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
init_cache_database ( & db_cache ) . unwrap ( ) ;
let data_file = NamedTempFile ::new ( ) . unwrap ( ) ;
2020-08-05 18:14:45 -07:00
let db_data = DataConnection ( Connection ::open ( data_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
init_data_database ( & db_data ) . unwrap ( ) ;
// Add an account to the wallet
let extsk = ExtendedSpendingKey ::master ( & [ ] ) ;
let extfvk = ExtendedFullViewingKey ::from ( & extsk ) ;
2020-08-05 13:27:40 -07:00
init_accounts_table ( & db_data , & tests ::network ( ) , & [ extfvk . clone ( ) ] ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Add funds to the wallet in a single note
let value = Amount ::from_u64 ( 50000 ) . unwrap ( ) ;
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) ,
2019-03-08 19:20:32 -08:00
BlockHash ( [ 0 ; 32 ] ) ,
extfvk . clone ( ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Verified balance matches total balance
2020-08-05 18:14:45 -07:00
assert_eq! ( get_balance ( & db_data , 0 ) . unwrap ( ) , value ) ;
assert_eq! ( get_verified_balance ( & db_data , 0 ) . unwrap ( ) , value ) ;
2019-03-08 19:20:32 -08:00
// Add more funds to the wallet in a second note
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) + 1 ,
2019-03-08 19:20:32 -08:00
cb . hash ( ) ,
extfvk . clone ( ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Verified balance does not include the second note
2020-08-05 18:14:45 -07:00
assert_eq! ( get_balance ( & db_data , 0 ) . unwrap ( ) , value + value ) ;
assert_eq! ( get_verified_balance ( & db_data , 0 ) . unwrap ( ) , value ) ;
2019-03-08 19:20:32 -08:00
// Spend fails because there are insufficient verified notes
let extsk2 = ExtendedSpendingKey ::master ( & [ ] ) ;
2019-05-24 07:59:18 -07:00
let to = extsk2 . default_address ( ) . unwrap ( ) . 1. into ( ) ;
2019-03-08 19:20:32 -08:00
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 70000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! (
e . to_string ( ) ,
2020-11-23 17:50:47 -08:00
" Insufficient balance (have 50000, need 71000 including fee) "
2019-03-08 19:20:32 -08:00
) ,
}
// 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 (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) + i ,
2019-03-08 19:20:32 -08:00
cb . hash ( ) ,
extfvk . clone ( ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
2019-03-08 19:20:32 -08:00
}
2020-08-05 18:14:45 -07:00
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Second spend still fails
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 70000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! (
e . to_string ( ) ,
2020-11-23 17:50:47 -08:00
" Insufficient balance (have 50000, need 71000 including fee) "
2019-03-08 19:20:32 -08:00
) ,
}
// Mine block 11 so that the second note becomes verified
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) + 10 ,
2019-03-08 19:20:32 -08:00
cb . hash ( ) ,
extfvk . clone ( ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Second spend should now succeed
create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 70000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
)
. unwrap ( ) ;
}
#[ test ]
fn create_to_address_fails_on_locked_notes ( ) {
let cache_file = NamedTempFile ::new ( ) . unwrap ( ) ;
2020-08-05 18:14:45 -07:00
let db_cache = CacheConnection ( Connection ::open ( cache_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
init_cache_database ( & db_cache ) . unwrap ( ) ;
let data_file = NamedTempFile ::new ( ) . unwrap ( ) ;
2020-08-05 18:14:45 -07:00
let db_data = DataConnection ( Connection ::open ( data_file . path ( ) ) . unwrap ( ) ) ;
2019-03-08 19:20:32 -08:00
init_data_database ( & db_data ) . unwrap ( ) ;
// Add an account to the wallet
let extsk = ExtendedSpendingKey ::master ( & [ ] ) ;
let extfvk = ExtendedFullViewingKey ::from ( & extsk ) ;
2020-08-05 13:27:40 -07:00
init_accounts_table ( & db_data , & tests ::network ( ) , & [ extfvk . clone ( ) ] ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Add funds to the wallet in a single note
let value = Amount ::from_u64 ( 50000 ) . unwrap ( ) ;
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) ,
2019-03-08 19:20:32 -08:00
BlockHash ( [ 0 ; 32 ] ) ,
extfvk . clone ( ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
assert_eq! ( get_balance ( & db_data , 0 ) . unwrap ( ) , value ) ;
2019-03-08 19:20:32 -08:00
// Send some of the funds to another address
let extsk2 = ExtendedSpendingKey ::master ( & [ ] ) ;
2019-05-24 07:59:18 -07:00
let to = extsk2 . default_address ( ) . unwrap ( ) . 1. into ( ) ;
2019-03-08 19:20:32 -08:00
create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 15000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
)
. unwrap ( ) ;
// A second spend fails because there are no usable notes
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 2000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! (
e . to_string ( ) ,
2020-11-23 17:50:47 -08:00
" Insufficient balance (have 0, need 3000 including fee) "
2019-03-08 19:20:32 -08:00
) ,
}
// 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 (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) + i ,
2019-03-08 19:20:32 -08:00
cb . hash ( ) ,
ExtendedFullViewingKey ::from ( & ExtendedSpendingKey ::master ( & [ i as u8 ] ) ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
2019-03-08 19:20:32 -08:00
}
2020-08-05 18:14:45 -07:00
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Second spend still fails
match create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 2000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
) {
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! (
e . to_string ( ) ,
2020-11-23 17:50:47 -08:00
" Insufficient balance (have 0, need 3000 including fee) "
2019-03-08 19:20:32 -08:00
) ,
}
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) + 22 ,
2019-03-08 19:20:32 -08:00
cb . hash ( ) ,
ExtendedFullViewingKey ::from ( & ExtendedSpendingKey ::master ( & [ 22 ] ) ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
scan_cached_blocks ( & tests ::network ( ) , & db_cache , & db_data , None ) . unwrap ( ) ;
2019-03-08 19:20:32 -08:00
// Second spend should now succeed
create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& tests ::network ( ) ,
2019-03-08 19:20:32 -08:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 2000 ) . unwrap ( ) ,
None ,
2020-07-09 04:48:09 -07:00
OvkPolicy ::Sender ,
2019-03-08 19:20:32 -08:00
)
. unwrap ( ) ;
}
2020-07-09 04:48:09 -07:00
#[ test ]
fn ovk_policy_prevents_recovery_from_chain ( ) {
2020-08-05 13:27:40 -07:00
let network = tests ::network ( ) ;
2020-07-09 04:48:09 -07:00
let cache_file = NamedTempFile ::new ( ) . unwrap ( ) ;
2020-08-05 18:14:45 -07:00
let db_cache = CacheConnection ( Connection ::open ( cache_file . path ( ) ) . unwrap ( ) ) ;
2020-07-09 04:48:09 -07:00
init_cache_database ( & db_cache ) . unwrap ( ) ;
let data_file = NamedTempFile ::new ( ) . unwrap ( ) ;
2020-08-05 18:14:45 -07:00
let db_data = DataConnection ( Connection ::open ( data_file . path ( ) ) . unwrap ( ) ) ;
2020-07-09 04:48:09 -07:00
init_data_database ( & db_data ) . unwrap ( ) ;
// Add an account to the wallet
let extsk = ExtendedSpendingKey ::master ( & [ ] ) ;
let extfvk = ExtendedFullViewingKey ::from ( & extsk ) ;
2020-08-05 13:27:40 -07:00
init_accounts_table ( & db_data , & network , & [ extfvk . clone ( ) ] ) . unwrap ( ) ;
2020-07-09 04:48:09 -07:00
// Add funds to the wallet in a single note
let value = Amount ::from_u64 ( 50000 ) . unwrap ( ) ;
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) ,
2020-07-09 04:48:09 -07:00
BlockHash ( [ 0 ; 32 ] ) ,
extfvk . clone ( ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
scan_cached_blocks ( & network , & db_cache , & db_data , None ) . unwrap ( ) ;
assert_eq! ( get_balance ( & db_data , 0 ) . unwrap ( ) , value ) ;
2020-07-09 04:48:09 -07:00
let extsk2 = ExtendedSpendingKey ::master ( & [ ] ) ;
let addr2 = extsk2 . default_address ( ) . unwrap ( ) . 1 ;
let to = addr2 . clone ( ) . into ( ) ;
let send_and_recover_with_policy = | ovk_policy | {
let tx_row = create_to_address (
2020-08-05 18:14:45 -07:00
& db_data ,
2020-08-05 13:27:40 -07:00
& network ,
2020-07-09 04:48:09 -07:00
consensus ::BranchId ::Blossom ,
test_prover ( ) ,
( 0 , & extsk ) ,
& to ,
Amount ::from_u64 ( 15000 ) . unwrap ( ) ,
None ,
ovk_policy ,
)
. unwrap ( ) ;
// Fetch the transaction from the database
2020-08-05 18:14:45 -07:00
let raw_tx : Vec < _ > = db_data
. 0
2020-07-09 04:48:09 -07:00
. query_row (
" SELECT raw FROM transactions
WHERE id_tx = ? " ,
& [ tx_row ] ,
| row | row . get ( 0 ) ,
)
. unwrap ( ) ;
let tx = Transaction ::read ( & raw_tx [ .. ] ) . unwrap ( ) ;
// Fetch the output index from the database
2020-08-05 18:14:45 -07:00
let output_index : i64 = db_data
. 0
2020-07-09 04:48:09 -07:00
. query_row (
" SELECT output_index FROM sent_notes
WHERE tx = ? " ,
& [ tx_row ] ,
| row | row . get ( 0 ) ,
)
. unwrap ( ) ;
let output = & tx . shielded_outputs [ output_index as usize ] ;
2020-08-05 13:27:40 -07:00
try_sapling_output_recovery (
& network ,
sapling_activation_height ( ) ,
2020-07-09 04:48:09 -07:00
& extfvk . fvk . ovk ,
& output . cv ,
& output . cmu ,
2020-09-09 16:39:21 -07:00
& output . ephemeral_key ,
2020-07-09 04:48:09 -07:00
& output . enc_ciphertext ,
& output . out_ciphertext ,
)
} ;
// Send some of the funds to another address, keeping history.
// The recipient output is decryptable by the sender.
let ( _ , recovered_to , _ ) = send_and_recover_with_policy ( OvkPolicy ::Sender ) . unwrap ( ) ;
assert_eq! ( & recovered_to , & addr2 ) ;
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 22 (that don't send us funds)
// so that the first transaction expires
for i in 1 ..= 22 {
let ( cb , _ ) = fake_compact_block (
2020-08-05 13:27:40 -07:00
sapling_activation_height ( ) + i ,
2020-07-09 04:48:09 -07:00
cb . hash ( ) ,
ExtendedFullViewingKey ::from ( & ExtendedSpendingKey ::master ( & [ i as u8 ] ) ) ,
value ,
) ;
2020-08-05 18:14:45 -07:00
insert_into_cache ( & db_cache , & cb ) ;
2020-07-09 04:48:09 -07:00
}
2020-08-05 18:14:45 -07:00
scan_cached_blocks ( & network , & db_cache , & db_data , None ) . unwrap ( ) ;
2020-07-09 04:48:09 -07:00
// Send the funds again, discarding history.
// Neither transaction output is decryptable by the sender.
assert! ( send_and_recover_with_policy ( OvkPolicy ::Discard ) . is_none ( ) ) ;
}
2019-03-08 19:20:32 -08:00
}