Merge pull request #894 from nuttycom/sbs/select_only_witnessable

zcash_client_sqlite: Only select notes for which witnesses can be constructed.
This commit is contained in:
str4d 2023-08-18 01:15:50 +01:00 committed by GitHub
commit f61d60bb96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 169 additions and 18 deletions

View File

@ -353,14 +353,14 @@ mod tests {
use zcash_client_backend::{
address::RecipientAddress,
data_api::WalletRead,
data_api::{scanning::ScanPriority, WalletRead},
encoding::{encode_extended_full_viewing_key, encode_payment_address},
keys::{sapling, UnifiedFullViewingKey, UnifiedSpendingKey},
};
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, BranchId, Parameters},
consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters},
transaction::{TransactionData, TxVersion},
zip32::sapling::ExtendedFullViewingKey,
};
@ -368,6 +368,7 @@ mod tests {
use crate::{
error::SqliteClientError,
tests::{self, network},
wallet::scanning::priority_code,
AccountId, WalletDb,
};
@ -558,6 +559,33 @@ mod tests {
}
let expected_views = vec![
// v_sapling_shard_unscanned_ranges
format!(
"CREATE VIEW v_sapling_shard_unscanned_ranges AS
SELECT
shard.shard_index,
shard.shard_index << 16 AS start_position,
(shard.shard_index + 1) << 16 AS end_position_exclusive,
IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height,
shard.subtree_end_height AS subtree_end_height,
shard.contains_marked,
scan_queue.block_range_start,
scan_queue.block_range_end,
scan_queue.priority
FROM sapling_tree_shards shard
LEFT OUTER JOIN sapling_tree_shards prev_shard
ON shard.shard_index = prev_shard.shard_index + 1
INNER JOIN scan_queue ON
(scan_queue.block_range_start BETWEEN subtree_start_height AND shard.subtree_end_height) OR
((scan_queue.block_range_end - 1) BETWEEN subtree_start_height AND shard.subtree_end_height) OR
(
scan_queue.block_range_start <= prev_shard.subtree_end_height
AND (scan_queue.block_range_end - 1) >= shard.subtree_end_height
)
WHERE scan_queue.priority != {}",
u32::from(tests::network().activation_height(NetworkUpgrade::Sapling).unwrap()),
priority_code(&ScanPriority::Scanned),
),
// v_transactions
"CREATE VIEW v_transactions AS
WITH
@ -649,7 +677,7 @@ mod tests {
LEFT JOIN sent_note_counts
ON sent_note_counts.account_id = notes.account_id
AND sent_note_counts.id_tx = notes.id_tx
GROUP BY notes.account_id, transactions.id_tx",
GROUP BY notes.account_id, transactions.id_tx".to_owned(),
// v_tx_outputs
"CREATE VIEW v_tx_outputs AS
SELECT sapling_received_notes.tx AS id_tx,
@ -693,7 +721,7 @@ mod tests {
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
WHERE sapling_received_notes.is_change IS NULL
OR sapling_received_notes.is_change = 0"
OR sapling_received_notes.is_change = 0".to_owned(),
];
let mut views_query = db_data
@ -706,7 +734,7 @@ mod tests {
let sql: String = row.get(0).unwrap();
assert_eq!(
re.replace_all(&sql, " "),
re.replace_all(expected_views[expected_idx], " ")
re.replace_all(&expected_views[expected_idx], " ")
);
expected_idx += 1;
}
@ -971,7 +999,7 @@ mod tests {
wdb.conn.execute(
"INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, :txid, :tx_bytes)",
named_params![
":txid": tx.txid().as_ref(),
":txid": tx.txid().as_ref(),
":tx_bytes": &tx_bytes[..]
],
)?;

View File

@ -9,6 +9,7 @@ mod sent_notes_to_internal;
mod shardtree_support;
mod ufvk_support;
mod utxos_table;
mod v_sapling_shard_unscanned_ranges;
mod v_transactions_net;
use schemer_rusqlite::RusqliteMigration;
@ -36,6 +37,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
// received_notes_nullable_nf
// / | \
// shardtree_support nullifier_map sapling_memo_consistency
// |
// v_sapling_shard_unscanned_ranges
vec![
Box::new(initial_setup::Migration {}),
Box::new(utxos_table::Migration {}),
@ -58,5 +61,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
Box::new(sapling_memo_consistency::Migration {
params: params.clone(),
}),
Box::new(v_sapling_shard_unscanned_ranges::Migration {
params: params.clone(),
}),
]
}

View File

@ -0,0 +1,77 @@
//! This migration adds a view that returns the un-scanned ranges associated with each sapling note
//! commitment tree shard.
use std::collections::HashSet;
use schemer_rusqlite::RusqliteMigration;
use uuid::Uuid;
use zcash_client_backend::data_api::{scanning::ScanPriority, SAPLING_SHARD_HEIGHT};
use zcash_primitives::consensus;
use crate::wallet::{init::WalletMigrationError, scanning::priority_code};
use super::shardtree_support;
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xfa934bdc_97b6_4980_8a83_b2cb1ac465fd);
pub(super) struct Migration<P> {
pub(super) params: P,
}
impl<P> schemer::Migration for Migration<P> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
[shardtree_support::MIGRATION_ID].into_iter().collect()
}
fn description(&self) -> &'static str {
"Adds a view that returns the un-scanned ranges associated with each sapling note commitment tree shard."
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
transaction.execute_batch(
&format!(
"CREATE VIEW v_sapling_shard_unscanned_ranges AS
SELECT
shard.shard_index,
shard.shard_index << {} AS start_position,
(shard.shard_index + 1) << {} AS end_position_exclusive,
IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height,
shard.subtree_end_height AS subtree_end_height,
shard.contains_marked,
scan_queue.block_range_start,
scan_queue.block_range_end,
scan_queue.priority
FROM sapling_tree_shards shard
LEFT OUTER JOIN sapling_tree_shards prev_shard
ON shard.shard_index = prev_shard.shard_index + 1
INNER JOIN scan_queue ON
(scan_queue.block_range_start BETWEEN subtree_start_height AND shard.subtree_end_height) OR
((scan_queue.block_range_end - 1) BETWEEN subtree_start_height AND shard.subtree_end_height) OR
(
scan_queue.block_range_start <= prev_shard.subtree_end_height
AND (scan_queue.block_range_end - 1) >= shard.subtree_end_height
)
WHERE scan_queue.priority != {}",
SAPLING_SHARD_HEIGHT,
SAPLING_SHARD_HEIGHT,
u32::from(self.params.activation_height(consensus::NetworkUpgrade::Sapling).unwrap()),
priority_code(&ScanPriority::Scanned),
)
)?;
Ok(())
}
fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
transaction.execute_batch("DROP VIEW v_sapling_shard_unscanned_ranges;")?;
Ok(())
}
}

View File

@ -132,14 +132,36 @@ pub(crate) fn get_spendable_sapling_notes(
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
let mut stmt_unscanned_tip = conn.prepare_cached(
"SELECT 1 FROM v_sapling_shard_unscanned_ranges
WHERE :anchor_height BETWEEN subtree_start_height AND IFNULL(subtree_end_height, :anchor_height)
AND block_range_start <= :anchor_height",
)?;
let mut unscanned =
stmt_unscanned_tip.query(named_params![":anchor_height": &u32::from(anchor_height),])?;
if unscanned.next()?.is_some() {
// if the tip shard has unscanned ranges below the anchor height, none of our notes can be
// spent
return Ok(vec![]);
}
let mut stmt_select_notes = conn.prepare_cached(
"SELECT id_note, diversifier, value, rcm, commitment_tree_position
FROM sapling_received_notes
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
WHERE account = :account
AND commitment_tree_position IS NOT NULL
AND spent IS NULL
AND transactions.block <= :anchor_height
AND id_note NOT IN rarray(:exclude)",
AND id_note NOT IN rarray(:exclude)
AND NOT EXISTS (
SELECT 1 FROM v_sapling_shard_unscanned_ranges unscanned
-- select all the unscanned ranges involving the shard containing this note
WHERE sapling_received_notes.commitment_tree_position >= unscanned.start_position
AND sapling_received_notes.commitment_tree_position < unscanned.end_position_exclusive
-- exclude unscanned ranges above the anchor height which don't affect spendability
AND unscanned.block_range_start <= :anchor_height
)",
)?;
let excluded: Vec<Value> = exclude.iter().map(|n| Value::from(n.0)).collect();
@ -164,10 +186,21 @@ pub(crate) fn select_spendable_sapling_notes(
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
let mut stmt_unscanned_tip = conn.prepare_cached(
"SELECT 1 FROM v_sapling_shard_unscanned_ranges
WHERE :anchor_height BETWEEN subtree_start_height AND IFNULL(subtree_end_height, :anchor_height)
AND block_range_start <= :anchor_height",
)?;
let mut unscanned =
stmt_unscanned_tip.query(named_params![":anchor_height": &u32::from(anchor_height),])?;
if unscanned.next()?.is_some() {
// if the tip shard has unscanned ranges below the anchor height, none of our notes can be
// spent
return Ok(vec![]);
}
// 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:
//
// value has been reached.
// 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
@ -188,11 +221,21 @@ pub(crate) fn select_spendable_sapling_notes(
SUM(value)
OVER (PARTITION BY account, spent ORDER BY id_note) AS so_far
FROM sapling_received_notes
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
INNER JOIN transactions
ON transactions.id_tx = sapling_received_notes.tx
WHERE account = :account
AND commitment_tree_position IS NOT NULL
AND spent IS NULL
AND transactions.block <= :anchor_height
AND id_note NOT IN rarray(:exclude)
AND NOT EXISTS (
SELECT 1 FROM v_sapling_shard_unscanned_ranges unscanned
-- select all the unscanned ranges involving the shard containing this note
WHERE sapling_received_notes.commitment_tree_position >= unscanned.start_position
AND sapling_received_notes.commitment_tree_position < unscanned.end_position_exclusive
-- exclude unscanned ranges above the anchor height which don't affect spendability
AND unscanned.block_range_start <= :anchor_height
)
)
SELECT id_note, diversifier, value, rcm, commitment_tree_position
FROM eligible WHERE so_far < :target_value
@ -370,7 +413,7 @@ pub(crate) mod tests {
block::BlockHash,
consensus::BranchId,
legacy::TransparentAddress,
memo::Memo,
memo::{Memo, MemoBytes},
sapling::{
note_encryption::try_sapling_output_recovery, prover::TxProver, Note, PaymentAddress,
},
@ -418,10 +461,7 @@ pub(crate) mod tests {
zcash_client_backend::{
data_api::wallet::shield_transparent_funds, wallet::WalletTransparentOutput,
},
zcash_primitives::{
memo::MemoBytes,
transaction::components::{amount::NonNegativeAmount, OutPoint, TxOut},
},
zcash_primitives::transaction::components::{amount::NonNegativeAmount, OutPoint, TxOut},
};
pub(crate) fn test_prover() -> impl TxProver {
@ -555,8 +595,8 @@ pub(crate) mod tests {
let mut stmt_sent_notes = db_data
.conn
.prepare(
"SELECT output_index
FROM sent_notes
"SELECT output_index
FROM sent_notes
JOIN transactions ON transactions.id_tx = sent_notes.tx
WHERE transactions.txid = ?",
)