diff --git a/Cargo.lock b/Cargo.lock index ab7f13404..19dd79adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3088,6 +3088,7 @@ dependencies = [ "zcash_note_encryption", "zcash_primitives", "zcash_proofs", + "zcash_protocol", "zip32", ] diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index 6e4160d14..996b27997 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -24,6 +24,7 @@ zcash_client_backend = { workspace = true, features = ["unstable-serialization", zcash_encoding.workspace = true zcash_keys = { workspace = true, features = ["orchard", "sapling"] } zcash_primitives.workspace = true +zcash_protocol.workspace = true zip32.workspace = true # Dependencies exposed in a public API: diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index c877a2817..a506ac20c 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -115,6 +115,8 @@ pub(crate) const PRUNING_DEPTH: u32 = 100; pub(crate) const VERIFY_LOOKAHEAD: u32 = 10; pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling"; +#[cfg(feature = "orchard")] +pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard"; #[cfg(not(feature = "transparent-inputs"))] pub(crate) const UA_TRANSPARENT: bool = false; @@ -576,9 +578,12 @@ impl WalletWrite for WalletDb )?; note_positions.extend(block.transactions().iter().flat_map(|wtx| { - wtx.sapling_outputs() - .iter() - .map(|out| out.note_commitment_tree_position()) + wtx.sapling_outputs().iter().map(|out| { + ( + ShieldedProtocol::Sapling, + out.note_commitment_tree_position(), + ) + }) })); last_scanned_height = Some(block.height()); @@ -897,7 +902,7 @@ impl WalletCommitmentTrees for WalletDb; #[cfg(feature = "orchard")] - fn with_orchard_tree_mut(&mut self, _callback: F) -> Result + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result where for<'a> F: FnMut( &'a mut ShardTree< @@ -908,16 +913,41 @@ impl WalletCommitmentTrees for WalletDb Result, E: From>, { - todo!() + let tx = self + .conn + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let shard_store = SqliteShardStore::from_connection(&tx, ORCHARD_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + callback(&mut shardtree)? + }; + + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) } #[cfg(feature = "orchard")] fn put_orchard_subtree_roots( &mut self, - _start_index: u64, - _roots: &[CommitmentTreeRoot], + start_index: u64, + roots: &[CommitmentTreeRoot], ) -> Result<(), ShardTreeError> { - todo!() + let tx = self + .conn + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>( + &tx, + ORCHARD_TABLES_PREFIX, + start_index, + roots, + )?; + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(()) } } diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index f808eb2bd..c01121803 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -11,9 +11,8 @@ use uuid::Uuid; use zcash_client_backend::keys::AddressGenerationError; use zcash_primitives::{consensus, transaction::components::amount::BalanceError}; -use crate::WalletDb; - use super::commitment_tree; +use crate::WalletDb; mod migrations; @@ -262,6 +261,31 @@ mod tests { ON UPDATE RESTRICT, CONSTRAINT nf_uniq UNIQUE (spend_pool, nf) )", + "CREATE TABLE orchard_tree_cap ( + -- cap_id exists only to be able to take advantage of `ON CONFLICT` + -- upsert functionality; the table will only ever contain one row + cap_id INTEGER PRIMARY KEY, + cap_data BLOB NOT NULL + )", + "CREATE TABLE orchard_tree_checkpoint_marks_removed ( + checkpoint_id INTEGER NOT NULL, + mark_removed_position INTEGER NOT NULL, + FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id) + ON DELETE CASCADE, + CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) + )", + "CREATE TABLE orchard_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER + )", + "CREATE TABLE orchard_tree_shards ( + shard_index INTEGER PRIMARY KEY, + subtree_end_height INTEGER, + root_hash BLOB, + shard_data BLOB, + contains_marked INTEGER, + CONSTRAINT root_unique UNIQUE (root_hash) + )", r#"CREATE TABLE "sapling_received_notes" ( id INTEGER PRIMARY KEY, tx INTEGER NOT NULL, @@ -388,6 +412,52 @@ mod tests { } let expected_views = vec![ + // v_orchard_shard_scan_ranges + format!( + "CREATE VIEW v_orchard_shard_scan_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, + shard.contains_marked, + scan_queue.block_range_start, + scan_queue.block_range_end, + scan_queue.priority + FROM orchard_tree_shards shard + LEFT OUTER JOIN orchard_tree_shards prev_shard + ON shard.shard_index = prev_shard.shard_index + 1 + -- Join with scan ranges that overlap with the subtree's involved blocks. + INNER JOIN scan_queue ON ( + subtree_start_height < scan_queue.block_range_end AND + ( + scan_queue.block_range_start <= shard.subtree_end_height OR + shard.subtree_end_height IS NULL + ) + )", + u32::from(st.network().activation_height(NetworkUpgrade::Nu5).unwrap()), + ), + //v_orchard_shard_unscanned_ranges + format!( + "CREATE VIEW v_orchard_shard_unscanned_ranges AS + WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts) + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + block_range_start, + block_range_end, + priority + FROM v_orchard_shard_scan_ranges + INNER JOIN wallet_birthday + WHERE priority > {} + AND block_range_end > wallet_birthday.height", + priority_code(&ScanPriority::Scanned), + ), // v_sapling_shard_scan_ranges format!( "CREATE VIEW v_sapling_shard_scan_ranges AS diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index 1124afcf3..08dd86342 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -5,6 +5,7 @@ mod addresses_table; mod full_account_ids; mod initial_setup; mod nullifier_map; +mod orchard_shardtree; mod received_notes_nullable_nf; mod receiving_key_scopes; mod sapling_memo_consistency; @@ -24,7 +25,7 @@ use std::rc::Rc; use schemer_rusqlite::RusqliteMigration; use secrecy::SecretVec; -use zcash_primitives::consensus; +use zcash_protocol::consensus; use super::WalletMigrationError; @@ -45,20 +46,20 @@ pub(super) fn all_migrations( // | // v_transactions_net // | - // received_notes_nullable_nf - // / | \ - // / | \ - // shardtree_support sapling_memo_consistency nullifier_map - // / \ \ - // add_account_birthdays receiving_key_scopes v_transactions_transparent_history - // | \ | | - // v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false - // | \ | | - // wallet_summaries \ | v_transactions_shielding_balance - // \ | | - // \ | v_transactions_note_uniqueness - // \ | / - // full_account_ids + // received_notes_nullable_nf------ + // / | \ + // / | \ + // --------------- shardtree_support sapling_memo_consistency nullifier_map + // / / \ \ + // orchard_shardtree add_account_birthdays receiving_key_scopes v_transactions_transparent_history + // | \ | | + // v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false + // | \ | | + // wallet_summaries \ | v_transactions_shielding_balance + // \ | | + // \ | v_transactions_note_uniqueness + // \ | / + // full_account_ids vec![ Box::new(initial_setup::Migration {}), Box::new(utxos_table::Migration {}), @@ -98,5 +99,8 @@ pub(super) fn all_migrations( params: params.clone(), }), Box::new(full_account_ids::Migration { seed }), + Box::new(orchard_shardtree::Migration { + params: params.clone(), + }), ] } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs b/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs new file mode 100644 index 000000000..3cfa1f4f7 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs @@ -0,0 +1,194 @@ +//! This migration adds tables to the wallet database that are needed to persist Orchard note +//! commitment tree data using the `shardtree` crate. + +use rusqlite::{self, named_params, OptionalExtension}; +use schemer_rusqlite::RusqliteMigration; +use std::collections::HashSet; +use tracing::debug; +use uuid::Uuid; + +use zcash_client_backend::data_api::scanning::ScanPriority; +use zcash_protocol::consensus::{self, BlockHeight, NetworkUpgrade}; + +use super::shardtree_support; +use crate::wallet::{init::WalletMigrationError, scan_queue_extrema, scanning::priority_code}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x3a6487f7_e068_42bb_9d12_6bb8dbe6da00); + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [shardtree_support::MIGRATION_ID].into_iter().collect() + } + + fn description(&self) -> &'static str { + "Add support for storage of Orchard note commitment tree data using the `shardtree` crate." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // Add shard persistence + debug!("Creating tables for Orchard shard persistence"); + transaction.execute_batch( + "CREATE TABLE orchard_tree_shards ( + shard_index INTEGER PRIMARY KEY, + subtree_end_height INTEGER, + root_hash BLOB, + shard_data BLOB, + contains_marked INTEGER, + CONSTRAINT root_unique UNIQUE (root_hash) + ); + CREATE TABLE orchard_tree_cap ( + -- cap_id exists only to be able to take advantage of `ON CONFLICT` + -- upsert functionality; the table will only ever contain one row + cap_id INTEGER PRIMARY KEY, + cap_data BLOB NOT NULL + );", + )?; + + // Add checkpoint persistence + debug!("Creating tables for checkpoint persistence"); + transaction.execute_batch( + "CREATE TABLE orchard_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER + ); + CREATE TABLE orchard_tree_checkpoint_marks_removed ( + checkpoint_id INTEGER NOT NULL, + mark_removed_position INTEGER NOT NULL, + FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id) + ON DELETE CASCADE, + CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) + );", + )?; + + transaction.execute_batch(&format!( + "CREATE VIEW v_orchard_shard_scan_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, + shard.contains_marked, + scan_queue.block_range_start, + scan_queue.block_range_end, + scan_queue.priority + FROM orchard_tree_shards shard + LEFT OUTER JOIN orchard_tree_shards prev_shard + ON shard.shard_index = prev_shard.shard_index + 1 + -- Join with scan ranges that overlap with the subtree's involved blocks. + INNER JOIN scan_queue ON ( + subtree_start_height < scan_queue.block_range_end AND + ( + scan_queue.block_range_start <= shard.subtree_end_height OR + shard.subtree_end_height IS NULL + ) + )", + 16, // ORCHARD_SHARD_HEIGHT is only available when `feature = "orchard"` is enabled. + 16, // ORCHARD_SHARD_HEIGHT is only available when `feature = "orchard"` is enabled. + u32::from(self.params.activation_height(NetworkUpgrade::Nu5).unwrap()), + ))?; + + transaction.execute_batch(&format!( + "CREATE VIEW v_orchard_shard_unscanned_ranges AS + WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts) + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + block_range_start, + block_range_end, + priority + FROM v_orchard_shard_scan_ranges + INNER JOIN wallet_birthday + WHERE priority > {} + AND block_range_end > wallet_birthday.height;", + priority_code(&ScanPriority::Scanned), + ))?; + + // Treat the current best-known chain tip height as the height to use for Orchard + // initialization, bounded below by NU5 activation. + if let Some(orchard_init_height) = scan_queue_extrema(transaction)?.and_then(|r| { + self.params + .activation_height(NetworkUpgrade::Nu5) + .map(|orchard_activation| std::cmp::max(orchard_activation, *r.end())) + }) { + // If a scan range exists that contains the Orchard init height, split it in two at the + // init height. + if let Some((start, end, range_priority)) = transaction + .query_row_and_then( + "SELECT block_range_start, block_range_end, priority + FROM scan_queue + WHERE block_range_start <= :orchard_init_height + AND block_range_end > :orchard_init_height", + named_params![":orchard_init_height": u32::from(orchard_init_height)], + |row| { + let start = BlockHeight::from(row.get::<_, u32>(0)?); + let end = BlockHeight::from(row.get::<_, u32>(1)?); + let range_priority: i64 = row.get(2)?; + Ok((start, end, range_priority)) + }, + ) + .optional()? + { + transaction.execute( + "DELETE from scan_queue WHERE block_range_start = :start", + named_params![":start": u32::from(start)], + )?; + // Rewrite the start of the scan range to be exactly what it was prior to the + // change. + transaction.execute( + "INSERT INTO scan_queue (block_range_start, block_range_end, priority) + VALUES (:block_range_start, :block_range_end, :priority)", + named_params![ + ":block_range_start": u32::from(start), + ":block_range_end": u32::from(orchard_init_height), + ":priority": range_priority, + ], + )?; + // Rewrite the remainder of the range to have priority `Historic` + transaction.execute( + "INSERT INTO scan_queue (block_range_start, block_range_end, priority) + VALUES (:block_range_start, :block_range_end, :priority)", + named_params![ + ":block_range_start": u32::from(orchard_init_height), + ":block_range_end": u32::from(end), + ":priority": priority_code(&ScanPriority::Historic), + ], + )?; + // Rewrite any scanned ranges above the end of the first Orchard + // range to have priority `Historic` + transaction.execute( + "UPDATE scan_queue SET priority = :historic + WHERE :block_range_start >= :orchard_initial_range_end + AND priority = :scanned", + named_params![ + ":historic": priority_code(&ScanPriority::Historic), + ":orchard_initial_range_end": u32::from(end), + ":scanned": priority_code(&ScanPriority::Scanned), + ], + )?; + } + } + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs index 61f6cb903..924bcb018 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs @@ -1,6 +1,6 @@ -//! This migration adds tables to the wallet database that are needed to persist note commitment -//! tree data using the `shardtree` crate, and migrates existing witness data into these data -//! structures. +//! This migration adds tables to the wallet database that are needed to persist Sapling note +//! commitment tree data using the `shardtree` crate, and migrates existing witness data into these +//! data structures. use std::collections::{BTreeSet, HashSet}; diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index e67683745..91416444d 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -1,3 +1,4 @@ +use incrementalmerkletree::{Address, Position}; use rusqlite::{self, named_params, types::Value, OptionalExtension}; use shardtree::error::ShardTreeError; use std::cmp::{max, min}; @@ -6,22 +7,29 @@ use std::ops::Range; use std::rc::Rc; use tracing::{debug, trace}; -use incrementalmerkletree::{Address, Position}; -use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade}; - -use zcash_client_backend::data_api::{ - scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange}, - SAPLING_SHARD_HEIGHT, +use zcash_client_backend::{ + data_api::{ + scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange}, + SAPLING_SHARD_HEIGHT, + }, + ShieldedProtocol, }; +use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade}; use crate::{ error::SqliteClientError, wallet::{block_height_extrema, commitment_tree, init::WalletMigrationError}, - PRUNING_DEPTH, VERIFY_LOOKAHEAD, + PRUNING_DEPTH, SAPLING_TABLES_PREFIX, VERIFY_LOOKAHEAD, }; use super::wallet_birthday; +#[cfg(feature = "orchard")] +use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT}; + +#[cfg(not(feature = "orchard"))] +use zcash_client_backend::PoolType; + pub(crate) fn priority_code(priority: &ScanPriority) -> i64 { use ScanPriority::*; match priority { @@ -232,13 +240,75 @@ pub(crate) fn replace_queue_entries( Ok(()) } +fn extend_range( + conn: &rusqlite::Transaction<'_>, + range: &Range, + required_subtree_indices: BTreeSet, + table_prefix: &'static str, + fallback_start_height: Option, + birthday_height: Option, +) -> Result>, SqliteClientError> { + // we'll either have both min and max bounds, or we'll have neither + let subtree_index_bounds = required_subtree_indices + .iter() + .min() + .zip(required_subtree_indices.iter().max()); + + let mut shard_end_stmt = conn.prepare_cached(&format!( + "SELECT subtree_end_height + FROM {}_tree_shards + WHERE shard_index = :shard_index", + table_prefix + ))?; + + let mut shard_end = |index: u64| -> Result, rusqlite::Error> { + Ok(shard_end_stmt + .query_row(named_params![":shard_index": index], |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }) + .optional()? + .flatten()) + }; + + // If no notes belonging to the wallet were found, we don't need to extend the scanning + // range suggestions to include the associated subtrees, and our bounds are just the + // scanned range. Otherwise, ensure that all shard ranges starting from the wallet + // birthday are included. + subtree_index_bounds + .map(|(min_idx, max_idx)| { + let range_min = if *min_idx > 0 { + // get the block height of the end of the previous shard + shard_end(*min_idx - 1)? + } else { + // our lower bound is going to be the fallback height + fallback_start_height + }; + + // bound the minimum to the wallet birthday + let range_min = range_min.map(|h| birthday_height.map_or(h, |b| std::cmp::max(b, h))); + + // Get the block height for the end of the current shard, and make it an + // exclusive end bound. + let range_max = shard_end(*max_idx)?.map(|end| end + 1); + + Ok::, rusqlite::Error>(Range { + start: range.start.min(range_min.unwrap_or(range.start)), + end: range.end.max(range_max.unwrap_or(range.end)), + }) + }) + .transpose() + .map_err(SqliteClientError::from) +} + pub(crate) fn scan_complete( conn: &rusqlite::Transaction<'_>, params: &P, range: Range, - wallet_note_positions: &[Position], + wallet_note_positions: &[(ShieldedProtocol, Position)], ) -> Result<(), SqliteClientError> { // Read the wallet birthday (if known). + // TODO: use per-pool birthdays? let wallet_birthday = wallet_birthday(conn)?; // Determine the range of block heights for which we will be updating the scan queue. @@ -247,63 +317,52 @@ pub(crate) fn scan_complete( // ranges starting from the wallet birthday to include the blocks needed to complete // the note commitment tree subtrees containing the positions of the discovered notes. // We will query by subtree index to find these bounds. - let required_subtrees = wallet_note_positions - .iter() - .map(|p| Address::above_position(SAPLING_SHARD_HEIGHT.into(), *p).index()) - .collect::>(); - - // we'll either have both min and max bounds, or we'll have neither - let subtree_bounds = required_subtrees - .iter() - .min() - .zip(required_subtrees.iter().max()); - - let mut sapling_shard_end_stmt = conn.prepare_cached( - "SELECT subtree_end_height - FROM sapling_tree_shards - WHERE shard_index = :shard_index", - )?; + let mut required_sapling_subtrees = BTreeSet::new(); + #[cfg(feature = "orchard")] + let mut required_orchard_subtrees = BTreeSet::new(); + for (protocol, position) in wallet_note_positions { + match protocol { + ShieldedProtocol::Sapling => { + required_sapling_subtrees.insert( + Address::above_position(SAPLING_SHARD_HEIGHT.into(), *position).index(), + ); + } + ShieldedProtocol::Orchard => { + #[cfg(feature = "orchard")] + required_orchard_subtrees.insert( + Address::above_position(ORCHARD_SHARD_HEIGHT.into(), *position).index(), + ); + + #[cfg(not(feature = "orchard"))] + return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded( + *protocol, + ))); + } + } + } - let mut sapling_shard_end = |index: u64| -> Result, rusqlite::Error> { - Ok(sapling_shard_end_stmt - .query_row(named_params![":shard_index": index], |row| { - row.get::<_, Option>(0) - .map(|opt| opt.map(BlockHeight::from)) - }) - .optional()? - .flatten()) - }; + let extended_range = extend_range( + conn, + &range, + required_sapling_subtrees, + SAPLING_TABLES_PREFIX, + params.activation_height(NetworkUpgrade::Sapling), + wallet_birthday, + )?; - // If no notes belonging to the wallet were found, we don't need to extend the scanning - // range suggestions to include the associated subtrees, and our bounds are just the - // scanned range. Otherwise, ensure that all shard ranges starting from the wallet - // birthday are included. - subtree_bounds - .map(|(min_idx, max_idx)| { - let range_min = if *min_idx > 0 { - // get the block height of the end of the previous shard - sapling_shard_end(*min_idx - 1)? - } else { - // our lower bound is going to be the Sapling activation height - params.activation_height(NetworkUpgrade::Sapling) - }; - - // bound the minimum to the wallet birthday - let range_min = - range_min.map(|h| wallet_birthday.map_or(h, |b| std::cmp::max(b, h))); - - // Get the block height for the end of the current shard, and make it an - // exclusive end bound. - let range_max = sapling_shard_end(*max_idx)?.map(|end| end + 1); - - Ok::, rusqlite::Error>(Range { - start: range.start.min(range_min.unwrap_or(range.start)), - end: range.end.max(range_max.unwrap_or(range.end)), - }) - }) - .transpose() - .map_err(SqliteClientError::from) - }?; + #[cfg(feature = "orchard")] + let extended_range = extend_range( + conn, + extended_range.as_ref().unwrap_or(&range), + required_orchard_subtrees, + ORCHARD_TABLES_PREFIX, + params.activation_height(NetworkUpgrade::Nu5), + wallet_birthday, + )? + .or(extended_range); + + extended_range + }; let query_range = extended_range.clone().unwrap_or_else(|| range.clone()); @@ -334,6 +393,20 @@ pub(crate) fn scan_complete( Ok(()) } +fn tip_shard_end_height( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, +) -> Result, rusqlite::Error> { + conn.query_row( + &format!( + "SELECT MAX(subtree_end_height) FROM {}_tree_shards", + table_prefix + ), + [], + |row| Ok(row.get::<_, Option>(0)?.map(BlockHeight::from)), + ) +} + pub(crate) fn update_chain_tip( conn: &rusqlite::Transaction<'_>, params: &P, @@ -372,18 +445,25 @@ pub(crate) fn update_chain_tip( let chain_end = new_tip + 1; // Read the maximum height from the shards table. - let shard_start_height = conn.query_row( - "SELECT MAX(subtree_end_height) - FROM sapling_tree_shards", - [], - |row| Ok(row.get::<_, Option>(0)?.map(BlockHeight::from)), - )?; + let sapling_shard_tip = tip_shard_end_height(conn, SAPLING_TABLES_PREFIX)?; + #[cfg(feature = "orchard")] + let orchard_shard_tip = tip_shard_end_height(conn, ORCHARD_TABLES_PREFIX)?; + + #[cfg(feature = "orchard")] + let min_shard_tip = match (sapling_shard_tip, orchard_shard_tip) { + (None, None) => None, + (None, Some(o)) => Some(o), + (Some(s), None) => Some(s), + (Some(s), Some(o)) => Some(std::cmp::min(s, o)), + }; + #[cfg(not(feature = "orchard"))] + let min_shard_tip = sapling_shard_tip; // Create a scanning range for the fragment of the last shard leading up to new tip. // We set a lower bound at the wallet birthday (if known), because account creation // requires specifying a tree frontier that ensures we don't need tree information // prior to the birthday. - let tip_shard_entry = shard_start_height.filter(|h| h < &chain_end).map(|h| { + let tip_shard_entry = min_shard_tip.filter(|h| h < &chain_end).map(|h| { let min_to_scan = wallet_birthday.filter(|b| b > &h).unwrap_or(h); ScanRange::from_parts(min_to_scan..chain_end, ScanPriority::ChainTip) });