1use std::{
83 collections::{HashMap, HashSet},
84 convert::TryFrom,
85 io::{self, Cursor},
86 num::NonZeroU32,
87 ops::{Range, RangeInclusive},
88 time::SystemTime,
89};
90
91use encoding::{
92 KeyScope, ReceiverFlags, account_kind_code, decode_diversifier_index_be,
93 encode_diversifier_index_be, memo_repr, parse_pool_code, pool_code,
94};
95use incrementalmerkletree::{Marking, Retention};
96use rusqlite::{self, Connection, OptionalExtension, named_params, params};
97use secrecy::{ExposeSecret, SecretVec};
98use shardtree::{error::ShardTreeError, store::ShardStore};
99use tracing::warn;
100use uuid::Uuid;
101
102use zcash_address::ZcashAddress;
103use zcash_client_backend::{
104 DecryptedOutput,
105 data_api::{
106 Account as _, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, AddressInfo,
107 AddressSource, BlockMetadata, Progress, Ratio, ReceivedTransactionOutput,
108 SAPLING_SHARD_HEIGHT, SentTransaction, SentTransactionOutput, TransactionDataRequest,
109 TransactionStatus, WalletSummary, Zip32Derivation,
110 anchor_retention::AnchorRetentionInterval,
111 chain::ChainState,
112 defaults::address_receiver_matches_ua,
113 error::{FindAccountForAddressError, RewindError},
114 scanning::{ScanPriority, ScanRange},
115 wallet::{ConfirmationsPolicy, TargetHeight},
116 },
117 wallet::{Note, NoteId, Recipient, WalletTx},
118};
119use zcash_keys::{
120 address::{Address, Receiver, UnifiedAddress},
121 encoding::AddressCodec,
122 keys::{
123 AddressGenerationError, ReceiverRequirement, UnifiedAddressRequest, UnifiedFullViewingKey,
124 UnifiedIncomingViewingKey, UnifiedSpendingKey,
125 },
126};
127use zcash_primitives::{
128 block::BlockHash,
129 merkle_tree::{HashSer, read_commitment_tree},
130 transaction::{Transaction, TransactionData, builder::DEFAULT_TX_EXPIRY_DELTA, fees::zip317},
131};
132use zcash_protocol::{
133 PoolType, ShieldedPool, TxId,
134 consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters, TxIndex},
135 memo::{Memo, MemoBytes},
136 value::{ZatBalance, Zatoshis},
137};
138use zip32::{DiversifierIndex, fingerprint::SeedFingerprint};
139
140use self::{
141 common::{TableConstants, table_constants},
142 scanning::{parse_priority_code, priority_code, replace_queue_entries},
143};
144use crate::{
145 AccountRef, AccountUuid, AddressRef, PRUNING_DEPTH, SqlTransaction, TransferType, TxRef,
146 WalletCommitmentTrees, WalletDb,
147 error::{BackendError, SqliteClientError},
148 util::Clock,
149 wallet::{
150 commitment_tree::{SqliteShardStore, get_max_checkpointed_height},
151 encoding::LEGACY_ADDRESS_INDEX_NULL,
152 },
153};
154
155#[cfg(feature = "transparent-inputs")]
156use {
157 crate::GapLimits,
158 ::transparent::{
159 bundle::{OutPoint, TxOut},
160 keys::{IncomingViewingKey as _, NonHardenedChildIndex, TransparentKeyScope},
161 },
162 ReceiverRequirement::*,
163 rusqlite::types::Value,
164 std::rc::Rc,
165 zcash_client_backend::{data_api::DecryptedTransaction, wallet::WalletTransparentOutput},
166};
167
168#[cfg(feature = "orchard")]
169use zcash_client_backend::data_api::{IRONWOOD_SHARD_HEIGHT, ORCHARD_SHARD_HEIGHT};
170
171use FindAccountForAddressError as E;
172#[cfg(feature = "zcashd-compat")]
173use {
174 crate::wallet::encoding::{decode_legacy_account_index, encode_legacy_account_index},
175 zcash_keys::keys::zcashd,
176};
177#[cfg(feature = "transparent-key-import")]
178use {
179 ::transparent::address::TransparentAddress,
180 zcash_script::{descriptor::sh, script::Evaluable},
181};
182
183pub mod commitment_tree;
184pub(crate) mod common;
185mod db;
186pub(crate) mod encoding;
187pub mod init;
188pub(crate) mod locking;
189#[cfg(feature = "orchard")]
190pub(crate) mod orchard;
191pub(crate) mod sapling;
192pub(crate) mod scanning;
193#[cfg(feature = "transparent-inputs")]
194pub(crate) mod transparent;
195
196pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0];
197
198pub(crate) const MIN_SHIELDED_DIVERSIFIER_OFFSET: u64 = 2817325936;
206
207fn parse_account_source(
208 account_kind: u32,
209 hd_seed_fingerprint: Option<[u8; 32]>,
210 hd_account_index: Option<u32>,
211 #[cfg(feature = "zcashd-compat")] legacy_account_index: i64,
212 spending_key_available: bool,
213 key_source: Option<String>,
214) -> Result<AccountSource, SqliteClientError> {
215 let derivation = hd_seed_fingerprint
216 .zip(hd_account_index)
217 .map(|(seed_fp, idx)| {
218 zip32::AccountId::try_from(idx).map_or_else(
219 |_| {
220 Err(SqliteClientError::CorruptedData(
221 "ZIP-32 account ID is out of range.".to_string(),
222 ))
223 },
224 |idx| {
225 Ok(Zip32Derivation::new(
226 SeedFingerprint::from_bytes(seed_fp),
227 idx,
228 #[cfg(feature = "zcashd-compat")]
229 decode_legacy_account_index(legacy_account_index)?,
230 ))
231 },
232 )
233 })
234 .transpose()?;
235
236 match (account_kind, derivation) {
237 (0, Some(derivation)) => Ok(AccountSource::Derived {
238 derivation,
239 key_source,
240 }),
241 (1, derivation) => Ok(AccountSource::Imported {
242 purpose: if spending_key_available {
243 AccountPurpose::Spending { derivation }
244 } else {
245 AccountPurpose::ViewOnly
246 },
247 key_source,
248 }),
249 (0, None) => Err(SqliteClientError::CorruptedData(
250 "Wallet DB account_kind constraint violated".to_string(),
251 )),
252 (_, _) => Err(SqliteClientError::CorruptedData(
253 "Unrecognized account_kind".to_string(),
254 )),
255 }
256}
257
258#[derive(Debug, Clone)]
260pub(crate) enum ViewingKey {
261 Full(Box<UnifiedFullViewingKey>),
266
267 Incoming(Box<UnifiedIncomingViewingKey>),
272}
273
274#[derive(Debug, Clone)]
276pub struct Account {
277 id: AccountRef,
278 uuid: AccountUuid,
279 name: Option<String>,
280 kind: AccountSource,
281 viewing_key: ViewingKey,
282 birthday: BlockHeight,
283}
284
285impl Account {
286 pub(crate) fn default_address(
292 &self,
293 request: UnifiedAddressRequest,
294 ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
295 self.uivk().default_address(request)
296 }
297
298 pub(crate) fn internal_id(&self) -> AccountRef {
299 self.id
300 }
301
302 pub(crate) fn birthday(&self) -> BlockHeight {
303 self.birthday
304 }
305}
306
307impl zcash_client_backend::data_api::Account for Account {
308 type AccountId = AccountUuid;
309
310 fn id(&self) -> AccountUuid {
311 self.uuid
312 }
313
314 fn name(&self) -> Option<&str> {
315 self.name.as_deref()
316 }
317
318 fn birthday_height(&self) -> BlockHeight {
319 self.birthday()
320 }
321
322 fn source(&self) -> &AccountSource {
323 &self.kind
324 }
325
326 fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
327 self.viewing_key.ufvk()
328 }
329
330 fn uivk(&self) -> UnifiedIncomingViewingKey {
331 self.viewing_key.uivk()
332 }
333}
334
335impl ViewingKey {
336 fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
337 match self {
338 ViewingKey::Full(ufvk) => Some(ufvk),
339 ViewingKey::Incoming(_) => None,
340 }
341 }
342
343 fn uivk(&self) -> UnifiedIncomingViewingKey {
344 match self {
345 ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(),
346 ViewingKey::Incoming(uivk) => uivk.as_ref().clone(),
347 }
348 }
349}
350
351struct IvkItemCache {
354 orchard: Option<Vec<u8>>,
355 sapling: Option<Vec<u8>>,
356 p2pkh: Option<Vec<u8>>,
357}
358
359impl IvkItemCache {
360 fn from_uivk(uivk: &UnifiedIncomingViewingKey) -> Self {
361 #[cfg(feature = "orchard")]
362 let orchard = uivk.orchard().as_ref().map(|k| k.to_bytes().to_vec());
363 #[cfg(not(feature = "orchard"))]
364 let orchard = None;
365
366 let sapling = uivk.sapling().as_ref().map(|k| k.to_bytes().to_vec());
367
368 #[cfg(feature = "transparent-inputs")]
369 let p2pkh = uivk.transparent().as_ref().map(|k| k.serialize());
370 #[cfg(not(feature = "transparent-inputs"))]
371 let p2pkh = None;
372
373 IvkItemCache {
374 orchard,
375 sapling,
376 p2pkh,
377 }
378 }
379}
380
381pub(crate) fn seed_matches_derived_account<P: consensus::Parameters>(
382 params: &P,
383 seed: &SecretVec<u8>,
384 seed_fingerprint: &SeedFingerprint,
385 account_index: zip32::AccountId,
386 uivk: &UnifiedIncomingViewingKey,
387) -> Result<bool, SqliteClientError> {
388 let seed_fingerprint_match =
389 &SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
390 SqliteClientError::BadAccountData(
391 "Seed must be between 32 and 252 bytes in length.".to_owned(),
392 )
393 })? == seed_fingerprint;
394
395 let uivk_match = {
399 let usk = UnifiedSpendingKey::from_seed(params, &seed.expose_secret()[..], account_index)
400 .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
401
402 let (seed_addr, _) = usk
403 .to_unified_full_viewing_key()
404 .default_address(UnifiedAddressRequest::AllAvailableKeys)?;
405 let (uivk_addr, _) = uivk.default_address(UnifiedAddressRequest::AllAvailableKeys)?;
406
407 #[cfg(not(feature = "orchard"))]
408 let orchard_match = false;
409 #[cfg(feature = "orchard")]
410 let orchard_match = seed_addr
411 .orchard()
412 .zip(uivk_addr.orchard())
413 .map(|(a, b)| a == b)
414 == Some(true);
415
416 let sapling_match = seed_addr
417 .sapling()
418 .zip(uivk_addr.sapling())
419 .map(|(a, b)| a == b)
420 == Some(true);
421
422 let p2pkh_match = seed_addr
423 .transparent()
424 .zip(uivk_addr.transparent())
425 .map(|(a, b)| a == b)
426 == Some(true);
427
428 orchard_match || sapling_match || p2pkh_match
429 };
430
431 if seed_fingerprint_match != uivk_match {
432 Err(SqliteClientError::CorruptedData(format!(
434 "Seed fingerprint match: {seed_fingerprint_match}, uivk match: {uivk_match}"
435 )))
436 } else {
437 Ok(seed_fingerprint_match && uivk_match)
438 }
439}
440
441pub(crate) fn max_zip32_account_index(
443 conn: &rusqlite::Connection,
444 seed_id: &SeedFingerprint,
445) -> Result<Option<zip32::AccountId>, SqliteClientError> {
446 conn.query_row_and_then(
447 "SELECT MAX(hd_account_index) FROM accounts WHERE hd_seed_fingerprint = :hd_seed",
448 [seed_id.to_bytes()],
449 |row| {
450 row.get::<_, Option<u32>>(0)?
451 .map(zip32::AccountId::try_from)
452 .transpose()
453 .map_err(|_| SqliteClientError::Zip32AccountIndexOutOfRange)
454 },
455 )
456}
457
458pub(crate) fn add_account<P: consensus::Parameters>(
459 conn: &rusqlite::Transaction,
460 params: &P,
461 account_name: &str,
462 kind: &AccountSource,
463 viewing_key: ViewingKey,
464 birthday: &AccountBirthday,
465 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
466) -> Result<Account, SqliteClientError> {
467 let uivk = viewing_key.uivk();
469 if let Some(existing_account) = get_account_for_uivk(conn, params, &uivk)? {
470 match (&viewing_key, existing_account.ufvk()) {
471 (ViewingKey::Full(new_ufvk), _) => {
472 return upgrade_account_ufvk(conn, params, &existing_account, new_ufvk);
475 }
476 (ViewingKey::Incoming(_), Some(_)) => {
477 return Err(SqliteClientError::AccountCollision(existing_account.id()));
480 }
481 (ViewingKey::Incoming(_), None) => {
482 return upgrade_account_uivk(conn, params, &existing_account, &uivk);
485 }
486 }
487 }
488
489 let account_uuid = AccountUuid(Uuid::new_v4());
490
491 let (derivation, spending_key_available, key_source) = match kind {
492 AccountSource::Derived {
493 derivation,
494 key_source,
495 } => (Some(derivation), true, key_source),
496 AccountSource::Imported {
497 purpose: AccountPurpose::Spending { derivation },
498 key_source,
499 } => (derivation.as_ref(), true, key_source),
500 AccountSource::Imported {
501 purpose: AccountPurpose::ViewOnly,
502 key_source,
503 } => (None, false, key_source),
504 };
505
506 let ivk_cache = IvkItemCache::from_uivk(&uivk);
507
508 let birthday_sapling_tree_size = Some(birthday.sapling_frontier().tree_size());
509 #[cfg(feature = "orchard")]
510 let birthday_orchard_tree_size = Some(birthday.orchard_frontier().tree_size());
511 #[cfg(not(feature = "orchard"))]
512 let birthday_orchard_tree_size: Option<u64> = None;
513
514 #[cfg(feature = "zcashd-compat")]
515 let zcashd_legacy_address_index =
516 encode_legacy_account_index(derivation.and_then(|d| d.legacy_address_index()));
517 #[cfg(not(feature = "zcashd-compat"))]
518 let zcashd_legacy_address_index: i64 = LEGACY_ADDRESS_INDEX_NULL;
519
520 let ufvk_encoded = viewing_key.ufvk().map(|ufvk| ufvk.encode(params));
521 let account_id = conn
522 .query_row(
523 r#"
524 INSERT INTO accounts (
525 name,
526 uuid,
527 account_kind, hd_seed_fingerprint, hd_account_index,
528 zcashd_legacy_address_index,
529 key_source,
530 ufvk, uivk,
531 orchard_ivk_item_cache, sapling_ivk_item_cache, p2pkh_ivk_item_cache,
532 birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size,
533 recover_until_height,
534 has_spend_key
535 )
536 VALUES (
537 :account_name,
538 :uuid,
539 :account_kind, :hd_seed_fingerprint, :hd_account_index,
540 :zcashd_legacy_address_index,
541 :key_source,
542 :ufvk, :uivk,
543 :orchard_ivk_item_cache, :sapling_ivk_item_cache, :p2pkh_ivk_item_cache,
544 :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size,
545 :recover_until_height,
546 :has_spend_key
547 )
548 RETURNING id
549 "#,
550 named_params![
551 ":account_name": account_name,
552 ":uuid": account_uuid.0,
553 ":account_kind": account_kind_code(kind),
554 ":hd_seed_fingerprint": derivation.map(|d| d.seed_fingerprint().to_bytes()),
555 ":hd_account_index": derivation.map(|d| u32::from(d.account_index())),
556 ":zcashd_legacy_address_index": zcashd_legacy_address_index,
557 ":key_source": key_source,
558 ":ufvk": ufvk_encoded,
559 ":uivk": uivk.encode(params),
560 ":orchard_ivk_item_cache": ivk_cache.orchard,
561 ":sapling_ivk_item_cache": ivk_cache.sapling,
562 ":p2pkh_ivk_item_cache": ivk_cache.p2pkh,
563 ":birthday_height": u32::from(birthday.height()),
564 ":birthday_sapling_tree_size": birthday_sapling_tree_size,
565 ":birthday_orchard_tree_size": birthday_orchard_tree_size,
566 ":recover_until_height": birthday.recover_until().map(u32::from),
567 ":has_spend_key": i64::from(spending_key_available),
568 ],
569 |row| row.get(0).map(AccountRef),
570 )
571 .map_err(|e| match e {
572 rusqlite::Error::SqliteFailure(f, s)
573 if f.code == rusqlite::ErrorCode::ConstraintViolation =>
574 {
575 if let Ok(colliding_uuid) = conn.query_row(
580 "SELECT uuid FROM accounts WHERE ufvk = ?",
581 params![ufvk_encoded],
582 |row| Ok(AccountUuid(row.get(0)?)),
583 ) {
584 return SqliteClientError::AccountCollision(colliding_uuid);
585 }
586
587 SqliteClientError::from(rusqlite::Error::SqliteFailure(f, s))
588 }
589 _ => SqliteClientError::from(e),
590 })?;
591
592 let account = Account {
593 id: account_id,
594 name: Some(account_name.to_owned()),
595 uuid: account_uuid,
596 kind: kind.clone(),
597 viewing_key,
598 birthday: birthday.height(),
599 };
600
601 match rewind_to_chain_state(
614 conn,
615 params,
616 #[cfg(feature = "transparent-inputs")]
617 gap_limits,
618 birthday.prior_chain_state(),
619 std::iter::once(account_uuid).collect(),
620 ) {
621 Ok(()) => {}
622 Err(RewindError::DataSource(e)) => return Err(e),
623 Err(RewindError::RewindBeyondBirthdays(_)) => {
624 unreachable!(
628 "rewind_to_chain_state cannot return RewindBeyondBirthdays with a non-empty \
629 reset_account_birthdays set"
630 );
631 }
632 Err(e) => {
636 return Err(SqliteClientError::BackendError(BackendError::Rewind(
637 Box::new(e),
638 )));
639 }
640 }
641
642 let sapling_activation_height = params
644 .activation_height(NetworkUpgrade::Sapling)
645 .unwrap_or_else(|| BlockHeight::from(0));
647
648 if sapling_activation_height < birthday.height() {
650 let ignored_range = sapling_activation_height..birthday.height();
651
652 replace_queue_entries::<SqliteClientError>(
653 conn,
654 &ignored_range,
655 Some(ScanRange::from_parts(
656 ignored_range.clone(),
657 ScanPriority::Ignored,
658 ))
659 .into_iter(),
660 false,
661 )?;
662 };
663
664 let (address, d_idx) = account.default_address(UnifiedAddressRequest::AllAvailableKeys)?;
668 upsert_address(
669 conn,
670 params,
671 account_id,
672 d_idx,
673 &address,
674 Some(birthday.height()),
675 false,
676 )?;
677
678 #[cfg(feature = "transparent-inputs")]
680 if let Ok(default_addr_idx) = NonHardenedChildIndex::try_from(d_idx) {
681 transparent::generate_address_range(
682 conn,
683 params,
684 account_id,
685 TransparentKeyScope::EXTERNAL,
686 UnifiedAddressRequest::ALLOW_ALL,
687 NonHardenedChildIndex::const_from_index(0)..default_addr_idx,
688 false,
689 )?
690 }
691
692 #[cfg(feature = "transparent-inputs")]
695 for key_scope in [
696 TransparentKeyScope::EXTERNAL,
697 TransparentKeyScope::INTERNAL,
698 TransparentKeyScope::EPHEMERAL,
699 ] {
700 transparent::generate_gap_addresses(
701 conn,
702 params,
703 gap_limits,
704 account_id,
705 key_scope,
706 UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
707 false,
708 )?;
709 }
710
711 Ok(account)
712}
713
714pub(crate) fn delete_account(
715 conn: &rusqlite::Transaction,
716 account_uuid: AccountUuid,
717) -> Result<(), SqliteClientError> {
718 let mut to_account_tx = conn.prepare(
721 r#"
722 SELECT
723 sn.id AS sent_note_id,
724 COALESCE(addresses.address, addresses.cached_transparent_receiver_address) AS to_address
725 FROM sent_notes sn
726 JOIN v_received_outputs ro ON ro.sent_note_id = sn.id
727 JOIN addresses ON addresses.id = ro.address_id
728 JOIN accounts ta ON ta.id = sn.to_account_id
729 WHERE ta.uuid = :account_uuid
730 "#,
731 )?;
732
733 let mut update_sent_note = conn.prepare(
734 r#"
735 UPDATE sent_notes
736 SET to_address = :to_address, to_account_id = NULL
737 WHERE id = :sent_note_id
738 "#,
739 )?;
740
741 let mut rows = to_account_tx.query(named_params![
742 ":account_uuid": account_uuid.0,
743 ])?;
744
745 while let Some(row) = rows.next()? {
746 if let Some(address) = row.get::<_, Option<String>>("to_address")? {
747 update_sent_note.execute(named_params![
748 ":sent_note_id": row.get::<_, i64>("sent_note_id")?,
749 ":to_address": address
750 ])?;
751 }
752 }
753
754 conn.execute(
758 r#"
759 WITH account_transactions AS (
760 SELECT ro.transaction_id
761 FROM v_received_outputs ro
762 JOIN accounts a ON a.id = ro.account_id
763 WHERE a.uuid = :account_uuid
764 UNION
765 SELECT ros.transaction_id
766 FROM v_received_output_spends ros
767 JOIN accounts sa ON sa.id = ros.account_id
768 WHERE sa.uuid = :account_uuid
769 ),
770 non_account_transactions AS (
771 SELECT ro.transaction_id
772 FROM v_received_outputs ro
773 JOIN accounts a ON a.id = ro.account_id
774 WHERE a.uuid != :account_uuid
775 UNION
776 SELECT ros.transaction_id
777 FROM v_received_output_spends ros
778 JOIN accounts sa ON sa.id = ros.account_id
779 WHERE sa.uuid != :account_uuid
780 )
781 DELETE FROM transactions WHERE id_tx IN (
782 SELECT transaction_id FROM account_transactions
783 EXCEPT
784 SELECT transaction_id FROM non_account_transactions
785 )
786 "#,
787 named_params![
788 ":account_uuid": account_uuid.0,
789 ],
790 )?;
791
792 conn.execute(
797 "DELETE FROM accounts WHERE uuid = :account_uuid",
798 named_params![
799 ":account_uuid": account_uuid.0,
800 ],
801 )?;
802
803 Ok(())
804}
805
806#[cfg(feature = "transparent-key-import")]
814pub(crate) fn transparent_receiver_address_exists(
815 conn: &rusqlite::Connection,
816 address: &str,
817) -> Result<bool, SqliteClientError> {
818 Ok(conn
819 .query_row(
820 "SELECT 1 FROM addresses WHERE cached_transparent_receiver_address = :address",
821 named_params![":address": address],
822 |_row| Ok(()),
823 )
824 .optional()?
825 .is_some())
826}
827
828#[cfg(feature = "transparent-key-import")]
833pub(crate) fn import_standalone_transparent_pubkey<P: consensus::Parameters>(
834 conn: &rusqlite::Transaction,
835 params: &P,
836 account_uuid: AccountUuid,
837 pubkey: secp256k1::PublicKey,
838) -> Result<usize, SqliteClientError> {
839 let account_id = get_account_ref(conn, account_uuid)?;
842 import_standalone_transparent_pubkey_inner(conn, params, account_uuid, account_id, pubkey)
843}
844
845#[cfg(feature = "transparent-key-import")]
849pub(crate) fn import_standalone_transparent_pubkeys<P: consensus::Parameters>(
850 conn: &rusqlite::Transaction,
851 params: &P,
852 account_uuid: AccountUuid,
853 pubkeys: &[secp256k1::PublicKey],
854) -> Result<usize, SqliteClientError> {
855 let account_id = get_account_ref(conn, account_uuid)?;
856 let mut inserted = 0;
857 for pubkey in pubkeys {
858 inserted += import_standalone_transparent_pubkey_inner(
859 conn,
860 params,
861 account_uuid,
862 account_id,
863 *pubkey,
864 )?;
865 }
866 Ok(inserted)
867}
868
869#[cfg(feature = "transparent-key-import")]
875fn import_standalone_transparent_pubkey_inner<P: consensus::Parameters>(
876 conn: &rusqlite::Transaction,
877 params: &P,
878 account_uuid: AccountUuid,
879 account_id: AccountRef,
880 pubkey: secp256k1::PublicKey,
881) -> Result<usize, SqliteClientError> {
882 let existing_import_account = conn
883 .query_row(
884 "SELECT accounts.uuid AS account_uuid
885 FROM addresses
886 JOIN accounts ON accounts.id = addresses.account_id
887 WHERE imported_transparent_receiver_pubkey = :imported_transparent_receiver_pubkey",
888 named_params![
889 ":imported_transparent_receiver_pubkey": pubkey.serialize()
890 ],
891 |row| row.get::<_, Uuid>("account_uuid"),
892 )
893 .optional()?;
894
895 if let Some(current) = existing_import_account {
896 if current == account_uuid.expose_uuid() {
897 return Ok(0);
899 } else {
900 return Err(SqliteClientError::StandaloneImportConflict(current));
901 }
902 }
903
904 let addr_str = Address::Transparent(TransparentAddress::from_pubkey(&pubkey)).encode(params);
905
906 if transparent_receiver_address_exists(conn, &addr_str)? {
914 return Ok(0);
915 }
916
917 let rows_affected = conn.execute(
918 r#"
919 INSERT INTO addresses (
920 account_id, key_scope, address, cached_transparent_receiver_address,
921 receiver_flags, imported_transparent_receiver_pubkey
922 )
923 VALUES (
924 :account_id, :key_scope, :address, :address,
925 :receiver_flags, :imported_transparent_receiver_pubkey
926 )
927 "#,
928 named_params![
929 ":account_id": account_id.0,
930 ":key_scope": KeyScope::Foreign.encode(),
931 ":address": addr_str,
932 ":receiver_flags": ReceiverFlags::P2PKH.bits(),
933 ":imported_transparent_receiver_pubkey": pubkey.serialize()
934 ],
935 )?;
936
937 Ok(rows_affected)
940}
941
942#[cfg(feature = "transparent-key-import")]
943pub(crate) fn import_standalone_transparent_script<P: consensus::Parameters>(
944 conn: &rusqlite::Transaction,
945 params: &P,
946 account_uuid: AccountUuid,
947 redeem_script: zcash_script::script::Redeem,
948) -> Result<(), SqliteClientError> {
949 let account_id = get_account_ref(conn, account_uuid)?;
952
953 const MAX_P2SH_REDEEM_SCRIPT_SIZE: usize = 520;
956 let rs_bytes = redeem_script.to_bytes();
957 if rs_bytes.len() > MAX_P2SH_REDEEM_SCRIPT_SIZE {
958 return Err(SqliteClientError::BadAccountData(format!(
959 "Redeem script exceeds maximum P2SH size of {MAX_P2SH_REDEEM_SCRIPT_SIZE} bytes (got {} bytes)",
960 rs_bytes.len()
961 )));
962 }
963
964 match zcash_script::solver::standard(&redeem_script) {
966 Some(zcash_script::solver::ScriptKind::MultiSig { .. }) => (),
967 _ => {
968 return Err(SqliteClientError::BadAccountData(
969 "Redeem script is not a supported P2SH script kind".to_owned(),
970 ));
971 }
972 }
973
974 let script_pubkey = sh(&redeem_script);
975 let addr = TransparentAddress::from_script_pubkey(&script_pubkey).ok_or_else(|| {
978 SqliteClientError::CorruptedData(
979 "Could not derive P2SH address from redeem script".to_owned(),
980 )
981 })?;
982
983 let existing_import_account = conn
984 .query_row(
985 "SELECT accounts.uuid AS account_uuid
986 FROM addresses
987 JOIN accounts ON accounts.id = addresses.account_id
988 WHERE imported_transparent_receiver_script = :imported_transparent_receiver_script",
989 named_params![
990 ":imported_transparent_receiver_script": &rs_bytes[..]
991 ],
992 |row| row.get::<_, Uuid>("account_uuid"),
993 )
994 .optional()?;
995
996 if let Some(current) = existing_import_account {
997 if current == account_uuid.expose_uuid() {
998 return Ok(());
1000 } else {
1001 return Err(SqliteClientError::StandaloneImportConflict(current));
1002 }
1003 }
1004
1005 let addr_str = Address::Transparent(addr).encode(params);
1006 conn.execute(
1007 r#"
1008 INSERT INTO addresses (
1009 account_id, key_scope, address, cached_transparent_receiver_address,
1010 receiver_flags, imported_transparent_receiver_script
1011 )
1012 VALUES (
1013 :account_id, :key_scope, :address, :address,
1014 :receiver_flags, :imported_transparent_receiver_script
1015 )
1016 "#,
1017 named_params![
1018 ":account_id": account_id.0,
1019 ":key_scope": KeyScope::Foreign.encode(),
1020 ":address": addr_str,
1021 ":receiver_flags": ReceiverFlags::P2SH.bits(),
1022 ":imported_transparent_receiver_script": &rs_bytes[..]
1023 ],
1024 )?;
1025
1026 Ok(())
1027}
1028
1029pub(crate) fn get_next_available_address<P: consensus::Parameters, C: Clock>(
1030 conn: &rusqlite::Transaction,
1031 params: &P,
1032 clock: &C,
1033 account_uuid: AccountUuid,
1034 request: UnifiedAddressRequest,
1035 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
1036) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, SqliteClientError> {
1037 let account: Account = match get_account(conn, params, account_uuid)? {
1038 Some(account) => account,
1039 None => {
1040 return Ok(None);
1041 }
1042 };
1043
1044 let requirements = account.uivk().receiver_requirements(request)?;
1046
1047 let (addr, diversifier_index) = if requirements.p2pkh() == ReceiverRequirement::Require {
1048 #[cfg(not(feature = "transparent-inputs"))]
1049 {
1050 return Err(SqliteClientError::AddressGeneration(
1051 AddressGenerationError::ReceiverTypeNotSupported(
1052 zcash_address::unified::Typecode::P2pkh,
1053 ),
1054 ));
1055 }
1056
1057 #[cfg(feature = "transparent-inputs")]
1060 {
1061 transparent::generate_gap_addresses(
1063 conn,
1064 params,
1065 gap_limits,
1066 account.internal_id(),
1067 TransparentKeyScope::EXTERNAL,
1068 UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1069 true,
1070 )?;
1071
1072 let (gap_start, addrs) = transparent::select_addrs_to_reserve(
1075 conn,
1076 params,
1077 account.internal_id(),
1078 TransparentKeyScope::EXTERNAL,
1079 gap_limits.external(),
1080 gap_limits
1081 .external()
1082 .try_into()
1083 .expect("gap limit fits in usize"),
1084 )?;
1085
1086 addrs
1088 .iter()
1089 .find_map(|(_, _, meta)| {
1090 meta.address_index()
1091 .map(DiversifierIndex::from)
1092 .and_then(|j| account.uivk().address(j, request).ok().map(|ua| (ua, j)))
1093 })
1094 .ok_or(SqliteClientError::ReachedGapLimit(
1095 TransparentKeyScope::EXTERNAL,
1096 gap_start.index() + gap_limits.external(),
1097 ))?
1098 }
1099 } else {
1100 let mut j = DiversifierIndex::from(
1102 clock
1103 .now()
1104 .duration_since(SystemTime::UNIX_EPOCH)
1105 .expect("system time is valid")
1106 .as_secs()
1107 .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET),
1108 );
1109
1110 let mut find_collision = conn.prepare(
1111 "SELECT exposed_at_height
1112 FROM addresses
1113 WHERE account_id = :account_id
1114 AND key_scope = :key_scope
1115 AND diversifier_index_be = :diversifier_index_be",
1116 )?;
1117
1118 loop {
1121 let found_addr = account.uivk().find_address(j, request)?;
1122 let collision = find_collision
1123 .query_row(
1124 named_params! {
1125 ":account_id": account.internal_id().0,
1126 ":key_scope": KeyScope::EXTERNAL.encode(),
1127 ":diversifier_index_be": &encode_diversifier_index_be(found_addr.1)
1128 },
1129 |row| row.get::<_, Option<u32>>(0),
1130 )
1131 .optional()?
1132 .flatten();
1133
1134 if collision.is_none() {
1135 break found_addr;
1136 } else {
1137 j.increment().map_err(|_| {
1138 SqliteClientError::AddressGeneration(
1139 AddressGenerationError::DiversifierSpaceExhausted,
1140 )
1141 })?;
1142 }
1143 }
1144 };
1145
1146 let chain_tip_height = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
1147 upsert_address(
1148 conn,
1149 params,
1150 account.internal_id(),
1151 diversifier_index,
1152 &addr,
1153 Some(chain_tip_height),
1154 true,
1155 )?;
1156
1157 Ok(Some((addr, diversifier_index)))
1158}
1159
1160pub(crate) fn list_addresses<P: consensus::Parameters>(
1161 conn: &rusqlite::Connection,
1162 params: &P,
1163 account_uuid: AccountUuid,
1164) -> Result<Vec<AddressInfo>, SqliteClientError> {
1165 let mut addrs = vec![];
1166
1167 let mut stmt_addrs = conn.prepare(
1168 "SELECT address, diversifier_index_be, key_scope
1169 FROM addresses
1170 JOIN accounts ON accounts.id = addresses.account_id
1171 WHERE accounts.uuid = :account_uuid
1172 AND exposed_at_height IS NOT NULL
1173 ORDER BY exposed_at_height ASC, diversifier_index_be ASC",
1174 )?;
1175
1176 let mut rows = stmt_addrs.query(named_params![
1177 ":account_uuid": account_uuid.0,
1178 ])?;
1179
1180 while let Some(row) = rows.next()? {
1181 let addr_str: String = row.get(0)?;
1182 let di_vec: Option<Vec<u8>> = row.get(1)?;
1183 let _scope = KeyScope::decode(row.get(2)?)?;
1184
1185 let addr = Address::decode(params, &addr_str).ok_or_else(|| {
1186 SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
1187 })?;
1188
1189 #[cfg(feature = "transparent-inputs")]
1191 let transparent_key_scope = matches!(addr, Address::Transparent(_) | Address::Tex(_))
1192 .then(|| _scope.into())
1193 .flatten();
1194
1195 let addr_source = match decode_diversifier_index_be(di_vec)? {
1196 Some(di) => Ok::<_, SqliteClientError>(AddressSource::Derived {
1197 diversifier_index: di,
1198 #[cfg(feature = "transparent-inputs")]
1199 transparent_key_scope,
1200 }),
1201 #[cfg(feature = "transparent-key-import")]
1202 None => Ok::<_, SqliteClientError>(AddressSource::Standalone),
1203 #[cfg(not(feature = "transparent-key-import"))]
1204 None => Err(SqliteClientError::CorruptedData(
1205 "diversifier index may not be null".to_string(),
1206 )),
1207 }?;
1208
1209 addrs.push(AddressInfo::from_parts(addr, addr_source).ok_or(
1210 SqliteClientError::CorruptedData(
1211 "transparent key scope information present for shielded address".to_string(),
1212 ),
1213 )?);
1214 }
1215
1216 Ok(addrs)
1217}
1218
1219pub(crate) fn find_account_for_address<P: consensus::Parameters>(
1249 conn: &rusqlite::Connection,
1250 params: &P,
1251 address: &Address,
1252) -> Result<Option<AccountUuid>, FindAccountForAddressError<SqliteClientError>> {
1253 let addr_str = address.encode(params);
1254 let taddr_str = match address {
1259 Address::Unified(ua) => ua
1260 .transparent()
1261 .map(|t| Address::Transparent(*t).encode(params)),
1262 _ => Some(addr_str.clone()),
1263 };
1264
1265 if let Some(acc) =
1266 find_account_by_exact_address(conn, &addr_str, taddr_str.as_deref()).map_err(E::Backend)?
1267 {
1268 return Ok(Some(acc));
1269 }
1270
1271 match address {
1272 Address::Unified(ua) => find_account_for_unified_address_algebraic(conn, params, ua),
1273 Address::Sapling(_) => {
1274 find_account_for_shielded_address(conn, params, address, ReceiverFlags::SAPLING)
1275 }
1276 _ => Ok(None),
1283 }
1284}
1285
1286fn find_account_by_exact_address(
1292 conn: &Connection,
1293 addr_str: &str,
1294 taddr_str: Option<&str>,
1295) -> Result<Option<AccountUuid>, SqliteClientError> {
1296 conn.query_row(
1297 "SELECT accounts.uuid
1298 FROM addresses
1299 JOIN accounts ON accounts.id = addresses.account_id
1300 WHERE address = :addr_str
1301 OR cached_transparent_receiver_address = :taddr_str
1302 LIMIT 1",
1303 named_params![
1304 ":addr_str": addr_str,
1305 ":taddr_str": taddr_str,
1306 ],
1307 |row| row.get::<_, Uuid>(0),
1308 )
1309 .optional()
1310 .map(|opt| opt.map(AccountUuid::from_uuid))
1311 .map_err(SqliteClientError::from)
1312}
1313
1314fn find_account_for_shielded_address<P: consensus::Parameters>(
1315 conn: &Connection,
1316 params: &P,
1317 address: &Address,
1318 shielded_flag: ReceiverFlags,
1319) -> Result<Option<AccountUuid>, FindAccountForAddressError<SqliteClientError>> {
1320 let mut stmt = conn
1323 .prepare_cached(
1324 "SELECT accounts.uuid, addresses.address
1325 FROM addresses
1326 JOIN accounts ON accounts.id = addresses.account_id
1327 WHERE (receiver_flags & :shielded_flag) != 0",
1328 )
1329 .map_err(|e| E::Backend(e.into()))?;
1330
1331 let mut rows = stmt
1332 .query(named_params![":shielded_flag": shielded_flag.bits()])
1333 .map_err(|e| E::Backend(e.into()))?;
1334
1335 while let Some(row) = rows
1336 .next()
1337 .map_err(|e| E::Backend(SqliteClientError::from(e)))?
1338 {
1339 let row_uuid: Uuid = row.get(0).map_err(|e| E::Backend(e.into()))?;
1340 let stored_addr_str: String = row.get(1).map_err(|e| E::Backend(e.into()))?;
1341 let stored = Address::decode(params, &stored_addr_str).ok_or_else(|| {
1342 E::Backend(SqliteClientError::CorruptedData(
1343 "Not a valid Zcash recipient address".to_owned(),
1344 ))
1345 })?;
1346 if let Address::Unified(stored_ua) = stored
1347 && address_receiver_matches_ua(address, &stored_ua, params)
1348 {
1349 return Ok(Some(AccountUuid::from_uuid(row_uuid)));
1350 }
1351 }
1352
1353 Ok(None)
1354}
1355
1356fn find_account_for_unified_address_algebraic<P: consensus::Parameters>(
1357 conn: &Connection,
1358 params: &P,
1359 unified_address: &UnifiedAddress,
1360) -> Result<Option<AccountUuid>, FindAccountForAddressError<SqliteClientError>> {
1361 let mut found_acc_id: Option<AccountUuid> = None;
1365 for acc_id in get_account_ids(conn).map_err(|e| E::Backend(e.into()))? {
1366 let Some(account) = get_account(conn, params, acc_id).map_err(E::Backend)? else {
1367 continue;
1368 };
1369 if !account
1370 .uivk()
1371 .decrypt_diversifiers(unified_address)
1372 .is_empty()
1373 {
1374 match found_acc_id {
1375 None => found_acc_id = Some(acc_id),
1376 Some(prev) if prev == acc_id => {}
1377 Some(_) => return Err(E::UnifiedAddressConflict),
1378 }
1379 }
1380 }
1381
1382 Ok(found_acc_id)
1383}
1384
1385pub(crate) fn get_last_generated_address_matching<P: consensus::Parameters>(
1386 conn: &rusqlite::Connection,
1387 params: &P,
1388 account_uuid: AccountUuid,
1389 address_filter: UnifiedAddressRequest,
1390) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, SqliteClientError> {
1391 let account: Account =
1392 get_account(conn, params, account_uuid)?.ok_or(SqliteClientError::AccountUnknown)?;
1393
1394 let requirements = account
1395 .uivk()
1396 .receiver_requirements(address_filter)
1397 .map_err(|_| {
1398 SqliteClientError::BadAccountData(
1399 "Could not generate UnifiedAddressRequest for UIVK".to_string(),
1400 )
1401 })?;
1402 let require_flags = ReceiverFlags::required(requirements);
1403 let omit_flags = ReceiverFlags::omitted(requirements);
1404 let addr: Option<(String, Option<Vec<u8>>)> = conn
1408 .query_row(
1409 "SELECT address, diversifier_index_be
1410 FROM addresses
1411 WHERE account_id = :account_id
1412 AND key_scope = :key_scope
1413 AND (receiver_flags & :require_flags) = :require_flags
1414 AND (receiver_flags & :omit_flags) = 0
1415 AND exposed_at_height IS NOT NULL
1416 ORDER BY exposed_at_height DESC, diversifier_index_be DESC
1417 LIMIT 1",
1418 named_params![
1419 ":account_id": account.internal_id().0,
1420 ":key_scope": KeyScope::EXTERNAL.encode(),
1421 ":require_flags": require_flags.bits(),
1422 ":omit_flags": omit_flags.bits(),
1423 ],
1424 |row| Ok((row.get(0)?, row.get(1)?)),
1425 )
1426 .optional()?;
1427
1428 addr.map(|(addr_str, di_vec)| {
1429 let diversifier_index = decode_diversifier_index_be(di_vec)?.ok_or_else(|| {
1430 SqliteClientError::CorruptedData(
1431 "Addresses in EXTERNAL scope must be HD-derived".to_owned(),
1432 )
1433 })?;
1434 Address::decode(params, &addr_str)
1435 .ok_or_else(|| {
1436 SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
1437 })
1438 .and_then(|addr| match addr {
1439 Address::Unified(ua) => Ok(ua),
1440 _ => Err(SqliteClientError::CorruptedData(format!(
1441 "Addresses table contains {addr_str} which is not a unified address",
1442 ))),
1443 })
1444 .map(|addr| (addr, diversifier_index))
1445 })
1446 .transpose()
1447}
1448
1449pub(crate) fn upsert_address<P: consensus::Parameters>(
1464 conn: &rusqlite::Connection,
1465 params: &P,
1466 account_id: AccountRef,
1467 diversifier_index: DiversifierIndex,
1468 address: &UnifiedAddress,
1469 exposed_at_height: Option<BlockHeight>,
1470 force_update_address: bool,
1471) -> Result<AddressRef, SqliteClientError> {
1472 let di_be = encode_diversifier_index_be(diversifier_index);
1474
1475 if force_update_address {
1479 let previously_exposed_as = conn
1480 .query_row(
1481 "SELECT address, exposed_at_height
1482 FROM addresses
1483 WHERE account_id = :account_id
1484 AND diversifier_index_be = :diversifier_index_be
1485 AND key_scope = :key_scope",
1486 named_params![
1487 ":account_id": account_id.0,
1488 ":diversifier_index_be": di_be,
1489 ":key_scope": KeyScope::EXTERNAL.encode(),
1490 ],
1491 |row| {
1492 let address = row.get::<_, String>("address")?;
1493 let exposed_at = row.get::<_, Option<u32>>("exposed_at_height")?;
1494 Ok(exposed_at.map(|_| address))
1495 },
1496 )
1497 .optional()?
1498 .flatten()
1499 .map(|addr_str| UnifiedAddress::decode(params, &addr_str))
1500 .transpose()
1501 .map_err(SqliteClientError::CorruptedData)?;
1502
1503 match previously_exposed_as {
1504 Some(addr) if &addr != address => {
1505 return Err(SqliteClientError::DiversifierIndexReuse(
1506 diversifier_index,
1507 Box::new(addr),
1508 ));
1509 }
1510 _ => (),
1511 }
1512 }
1513
1514 let mut stmt = conn.prepare_cached(
1515 "INSERT INTO addresses (
1516 account_id,
1517 diversifier_index_be,
1518 key_scope,
1519 address,
1520 transparent_child_index,
1521 cached_transparent_receiver_address,
1522 exposed_at_height,
1523 receiver_flags
1524 )
1525 VALUES (
1526 :account_id,
1527 :diversifier_index_be,
1528 :key_scope,
1529 :address,
1530 :transparent_child_index,
1531 :cached_transparent_receiver_address,
1532 :exposed_at_height,
1533 :receiver_flags
1534 )
1535 ON CONFLICT (account_id, diversifier_index_be, key_scope) DO UPDATE
1536 SET exposed_at_height = COALESCE(
1537 MIN(exposed_at_height, :exposed_at_height),
1538 exposed_at_height,
1539 :exposed_at_height
1540 ),
1541 address = IIF(
1542 exposed_at_height IS NULL AND :force_update_address,
1543 :address,
1544 address
1545 ),
1546 receiver_flags = IIF(
1547 exposed_at_height IS NULL AND :force_update_address,
1548 :receiver_flags,
1549 receiver_flags
1550 )
1551 RETURNING id",
1552 )?;
1553
1554 #[cfg(feature = "transparent-inputs")]
1555 let (transparent_child_index, cached_taddr) = {
1556 let idx = NonHardenedChildIndex::try_from(diversifier_index)
1557 .ok()
1558 .map(|i| i.index());
1559
1560 match (idx, address.transparent()) {
1562 (Some(idx), Some(r)) => Ok((Some(idx), Some(r.encode(params)))),
1563 (_, None) => Ok((None, None)),
1564 (None, Some(addr)) => Err(SqliteClientError::AddressNotRecognized(*addr)),
1565 }
1566 }?;
1567
1568 #[cfg(not(feature = "transparent-inputs"))]
1569 let (transparent_child_index, cached_taddr): (Option<u32>, Option<String>) = (None, None);
1570
1571 stmt.query_row(
1572 named_params![
1573 ":account_id": account_id.0,
1574 ":diversifier_index_be": &di_be,
1576 ":key_scope": KeyScope::EXTERNAL.encode(),
1577 ":address": &address.encode(params),
1578 ":transparent_child_index": transparent_child_index,
1579 ":cached_transparent_receiver_address": &cached_taddr,
1580 ":exposed_at_height": exposed_at_height.map(u32::from),
1581 ":force_update_address": force_update_address,
1582 ":receiver_flags": ReceiverFlags::from(address).bits()
1583 ],
1584 |row| row.get(0).map(AddressRef),
1585 )
1586 .map_err(SqliteClientError::from)
1587}
1588
1589#[cfg(feature = "transparent-inputs")]
1590pub(crate) fn involved_accounts(
1591 conn: &rusqlite::Connection,
1592 tx_refs: impl IntoIterator<Item = TxRef>,
1593) -> Result<HashSet<(AccountRef, AccountUuid, Option<TransparentKeyScope>)>, SqliteClientError> {
1594 let mut stmt = conn.prepare_cached(
1595 "SELECT account_id, accounts.uuid, key_scope
1596 FROM v_address_uses
1597 JOIN accounts ON accounts.id = v_address_uses.account_id
1598 WHERE transaction_id IN rarray(:tx_refs_ptr)",
1599 )?;
1600
1601 let tx_refs_values: Vec<Value> = tx_refs.into_iter().map(|r| Value::Integer(r.0)).collect();
1602 let tx_refs_ptr = Rc::new(tx_refs_values);
1603 let result = stmt
1604 .query_and_then(
1605 named_params! {
1606 ":tx_refs_ptr": &tx_refs_ptr
1607 },
1608 |row| {
1609 Ok::<_, SqliteClientError>((
1610 row.get("account_id").map(AccountRef)?,
1611 AccountUuid(row.get("uuid")?),
1612 KeyScope::decode(row.get("key_scope")?)?.as_transparent(),
1613 ))
1614 },
1615 )?
1616 .collect::<Result<HashSet<_>, _>>()?;
1617
1618 Ok(result)
1619}
1620
1621pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
1623 conn: &rusqlite::Connection,
1624 params: &P,
1625) -> Result<HashMap<AccountUuid, UnifiedFullViewingKey>, SqliteClientError> {
1626 let mut stmt_fetch_accounts = conn.prepare("SELECT uuid, ufvk FROM accounts")?;
1628
1629 let rows = stmt_fetch_accounts.query_map([], |row| {
1630 let ufvk_str: Option<String> = row.get(1)?;
1631 if let Some(ufvk_str) = ufvk_str {
1632 let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
1633 .map_err(SqliteClientError::CorruptedData);
1634 Ok(Some((AccountUuid(row.get(0)?), ufvk)))
1635 } else {
1636 Ok(None)
1637 }
1638 })?;
1639
1640 let mut res: HashMap<AccountUuid, UnifiedFullViewingKey> = HashMap::new();
1641 for row in rows {
1642 if let Some((account_id, ufvkr)) = row? {
1643 res.insert(account_id, ufvkr?);
1644 }
1645 }
1646
1647 Ok(res)
1648}
1649
1650fn parse_account_row<P: consensus::Parameters>(
1651 row: &rusqlite::Row<'_>,
1652 params: &P,
1653) -> Result<Account, SqliteClientError> {
1654 let account_id = AccountRef(row.get("id")?);
1655 let account_name = row.get("name")?;
1656 let account_uuid = AccountUuid(row.get("uuid")?);
1657 let kind = parse_account_source(
1658 row.get("account_kind")?,
1659 row.get("hd_seed_fingerprint")?,
1660 row.get("hd_account_index")?,
1661 #[cfg(feature = "zcashd-compat")]
1662 row.get("zcashd_legacy_address_index")?,
1663 row.get("has_spend_key")?,
1664 row.get("key_source")?,
1665 )?;
1666
1667 let ufvk_str: Option<String> = row.get("ufvk")?;
1668 let viewing_key = if let Some(ufvk_str) = ufvk_str {
1669 ViewingKey::Full(Box::new(
1670 UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
1671 SqliteClientError::CorruptedData(format!(
1672 "Could not decode unified full viewing key for account {}: {}",
1673 account_uuid.0, e
1674 ))
1675 })?,
1676 ))
1677 } else {
1678 let uivk_str: String = row.get("uivk")?;
1679 ViewingKey::Incoming(Box::new(
1680 UnifiedIncomingViewingKey::decode(params, &uivk_str).map_err(|e| {
1681 SqliteClientError::CorruptedData(format!(
1682 "Could not decode unified incoming viewing key for account {}: {}",
1683 account_uuid.0, e
1684 ))
1685 })?,
1686 ))
1687 };
1688
1689 let birthday = BlockHeight::from(row.get::<_, u32>("birthday_height")?);
1690
1691 Ok(Account {
1692 id: account_id,
1693 name: account_name,
1694 uuid: account_uuid,
1695 kind,
1696 viewing_key,
1697 birthday,
1698 })
1699}
1700
1701pub(crate) fn get_account<P: Parameters>(
1702 conn: &rusqlite::Connection,
1703 params: &P,
1704 account_uuid: AccountUuid,
1705) -> Result<Option<Account>, SqliteClientError> {
1706 let mut stmt = conn.prepare_cached(
1707 r#"
1708 SELECT id, name, uuid, account_kind,
1709 hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1710 ufvk, uivk, has_spend_key, birthday_height
1711 FROM accounts
1712 WHERE uuid = :account_uuid
1713 "#,
1714 )?;
1715
1716 let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>(
1717 named_params![":account_uuid": account_uuid.0],
1718 |row| parse_account_row(row, params),
1719 )?;
1720
1721 rows.next().transpose()
1722}
1723
1724#[cfg(feature = "transparent-inputs")]
1725pub(crate) fn get_account_internal<P: Parameters>(
1726 conn: &rusqlite::Connection,
1727 params: &P,
1728 account_id: AccountRef,
1729) -> Result<Option<Account>, SqliteClientError> {
1730 let mut stmt = conn.prepare_cached(
1731 r#"
1732 SELECT id, name, uuid, account_kind,
1733 hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1734 ufvk, uivk, has_spend_key, birthday_height
1735 FROM accounts
1736 WHERE id = :account_id
1737 "#,
1738 )?;
1739
1740 let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>(
1741 named_params![":account_id": account_id.0],
1742 |row| parse_account_row(row, params),
1743 )?;
1744
1745 rows.next().transpose()
1746}
1747
1748pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
1751 conn: &rusqlite::Connection,
1752 params: &P,
1753 ufvk: &UnifiedFullViewingKey,
1754) -> Result<Option<Account>, SqliteClientError> {
1755 let uivk = ufvk.to_unified_incoming_viewing_key();
1756 get_account_for_uivk(conn, params, &uivk)
1757}
1758
1759pub(crate) fn get_account_for_uivk<P: consensus::Parameters>(
1762 conn: &rusqlite::Connection,
1763 params: &P,
1764 uivk: &UnifiedIncomingViewingKey,
1765) -> Result<Option<Account>, SqliteClientError> {
1766 let ivk_cache = IvkItemCache::from_uivk(uivk);
1767
1768 let mut stmt = conn.prepare(
1769 "SELECT id, name, uuid, account_kind,
1770 hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1771 ufvk, uivk, has_spend_key, birthday_height
1772 FROM accounts
1773 WHERE orchard_ivk_item_cache = :orchard_ivk_item_cache
1774 OR sapling_ivk_item_cache = :sapling_ivk_item_cache
1775 OR p2pkh_ivk_item_cache = :p2pkh_ivk_item_cache",
1776 )?;
1777
1778 let accounts = stmt
1779 .query_and_then::<_, SqliteClientError, _, _>(
1780 named_params![
1781 ":orchard_ivk_item_cache": ivk_cache.orchard,
1782 ":sapling_ivk_item_cache": ivk_cache.sapling,
1783 ":p2pkh_ivk_item_cache": ivk_cache.p2pkh,
1784 ],
1785 |row| parse_account_row(row, params),
1786 )?
1787 .collect::<Result<Vec<_>, _>>()?;
1788
1789 if accounts.len() > 1 {
1790 Err(SqliteClientError::CorruptedData(
1791 "Multiple account records matched the provided UIVK".to_owned(),
1792 ))
1793 } else {
1794 Ok(accounts.into_iter().next())
1795 }
1796}
1797
1798fn upgrade_account_ufvk<P: consensus::Parameters>(
1804 conn: &rusqlite::Connection,
1805 params: &P,
1806 existing_account: &Account,
1807 ufvk: &UnifiedFullViewingKey,
1808) -> Result<Account, SqliteClientError> {
1809 let existing_uivk = existing_account.uivk();
1810
1811 if !ufvk.subsumes_uivk(&existing_uivk) {
1813 return Err(SqliteClientError::AccountCollision(existing_account.id()));
1814 }
1815
1816 if existing_account
1819 .ufvk()
1820 .is_some_and(|efvk| efvk.subsumes_ufvk(ufvk))
1821 {
1822 return Err(SqliteClientError::AccountCollision(existing_account.id()));
1823 }
1824
1825 let account_id = existing_account.internal_id();
1826 let ufvk_encoded = ufvk.encode(params);
1827 let uivk = ufvk.to_unified_incoming_viewing_key();
1828 let uivk_encoded = uivk.encode(params);
1829 let ivk_cache = IvkItemCache::from_uivk(&uivk);
1830
1831 conn.execute(
1832 "UPDATE accounts
1833 SET ufvk = :ufvk,
1834 uivk = :uivk,
1835 orchard_ivk_item_cache = :orchard_ivk,
1836 sapling_ivk_item_cache = :sapling_ivk,
1837 p2pkh_ivk_item_cache = :p2pkh_ivk
1838 WHERE id = :id",
1839 named_params![
1840 ":ufvk": ufvk_encoded,
1841 ":uivk": uivk_encoded,
1842 ":orchard_ivk": ivk_cache.orchard,
1843 ":sapling_ivk": ivk_cache.sapling,
1844 ":p2pkh_ivk": ivk_cache.p2pkh,
1845 ":id": account_id.0,
1846 ],
1847 )?;
1848
1849 let mut stmt = conn.prepare_cached(
1851 "SELECT id, name, uuid, account_kind,
1852 hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1853 ufvk, uivk, has_spend_key, birthday_height
1854 FROM accounts
1855 WHERE id = :account_id",
1856 )?;
1857 stmt.query_row(named_params![":account_id": account_id.0], |row| {
1858 Ok(parse_account_row(row, params))
1859 })?
1860}
1861
1862fn upgrade_account_uivk<P: consensus::Parameters>(
1868 conn: &rusqlite::Connection,
1869 params: &P,
1870 existing_account: &Account,
1871 uivk: &UnifiedIncomingViewingKey,
1872) -> Result<Account, SqliteClientError> {
1873 let existing_uivk = existing_account.uivk();
1874
1875 if !uivk.subsumes(&existing_uivk) || *uivk == existing_uivk {
1878 return Err(SqliteClientError::AccountCollision(existing_account.id()));
1879 }
1880
1881 let account_id = existing_account.internal_id();
1882 let uivk_encoded = uivk.encode(params);
1883
1884 let ivk_cache = IvkItemCache::from_uivk(uivk);
1885
1886 let rows_affected = conn.execute(
1887 "UPDATE accounts
1888 SET uivk = :uivk,
1889 orchard_ivk_item_cache = :orchard_ivk,
1890 sapling_ivk_item_cache = :sapling_ivk,
1891 p2pkh_ivk_item_cache = :p2pkh_ivk
1892 WHERE id = :id AND ufvk IS NULL",
1893 named_params![
1894 ":uivk": uivk_encoded,
1895 ":orchard_ivk": ivk_cache.orchard,
1896 ":sapling_ivk": ivk_cache.sapling,
1897 ":p2pkh_ivk": ivk_cache.p2pkh,
1898 ":id": account_id.0,
1899 ],
1900 )?;
1901 if rows_affected != 1 {
1902 return Err(SqliteClientError::CorruptedData(
1903 "UIVK upgrade failed: account already has a UFVK".to_owned(),
1904 ));
1905 }
1906
1907 let mut stmt = conn.prepare_cached(
1909 "SELECT id, name, uuid, account_kind,
1910 hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1911 ufvk, uivk, has_spend_key, birthday_height
1912 FROM accounts
1913 WHERE id = :account_id",
1914 )?;
1915 stmt.query_row(named_params![":account_id": account_id.0], |row| {
1916 Ok(parse_account_row(row, params))
1917 })?
1918}
1919
1920pub(crate) fn get_derived_account<P: consensus::Parameters>(
1923 conn: &rusqlite::Connection,
1924 params: &P,
1925 seed_fp: &SeedFingerprint,
1926 account_index: zip32::AccountId,
1927 #[cfg(feature = "zcashd-compat")] legacy_address_index: Option<zcashd::LegacyAddressIndex>,
1928) -> Result<Option<Account>, SqliteClientError> {
1929 let mut stmt = conn.prepare(&format!(
1930 "SELECT id, name, key_source, uuid, ufvk, birthday_height, zcashd_legacy_address_index
1931 FROM accounts
1932 WHERE hd_seed_fingerprint = :hd_seed_fingerprint
1933 AND hd_account_index = :hd_account_index
1934 AND (
1935 :zcashd_legacy_address_index = {LEGACY_ADDRESS_INDEX_NULL}
1936 OR zcashd_legacy_address_index = :zcashd_legacy_address_index
1937 )",
1938 ))?;
1939
1940 #[cfg(not(feature = "zcashd-compat"))]
1941 let legacy_address_index: i64 = LEGACY_ADDRESS_INDEX_NULL;
1942 #[cfg(feature = "zcashd-compat")]
1943 let legacy_address_index = encode_legacy_account_index(legacy_address_index);
1944
1945 let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>(
1946 named_params![
1947 ":hd_seed_fingerprint": seed_fp.to_bytes(),
1948 ":hd_account_index": u32::from(account_index),
1949 ":zcashd_legacy_address_index": legacy_address_index
1950 ],
1951 |row| {
1952 let account_id = AccountRef(row.get("id")?);
1953 let account_name = row.get("name")?;
1954 let key_source = row.get("key_source")?;
1955 let account_uuid = AccountUuid(row.get("uuid")?);
1956 let ufvk = match row.get::<_, Option<String>>("ufvk")? {
1957 None => Err(SqliteClientError::CorruptedData(format!(
1958 "Missing unified full viewing key for derived account {}",
1959 account_uuid.0,
1960 ))),
1961 Some(ufvk_str) => UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
1962 SqliteClientError::CorruptedData(format!(
1963 "Could not decode unified full viewing key for account {}: {}",
1964 account_uuid.0, e
1965 ))
1966 }),
1967 }?;
1968 let birthday = BlockHeight::from(row.get::<_, u32>("birthday_height")?);
1969 #[cfg(feature = "zcashd-compat")]
1970 let legacy_idx = decode_legacy_account_index(row.get("zcashd_legacy_address_index")?)?;
1971
1972 Ok(Account {
1973 id: account_id,
1974 name: account_name,
1975 uuid: account_uuid,
1976 kind: AccountSource::Derived {
1977 derivation: Zip32Derivation::new(
1978 *seed_fp,
1979 account_index,
1980 #[cfg(feature = "zcashd-compat")]
1981 legacy_idx,
1982 ),
1983 key_source,
1984 },
1985 viewing_key: ViewingKey::Full(Box::new(ufvk)),
1986 birthday,
1987 })
1988 },
1989 )?;
1990
1991 accounts.next().transpose()
1992}
1993
1994pub(crate) trait ProgressEstimator {
1995 fn sapling_scan_progress<P: consensus::Parameters>(
1996 &self,
1997 conn: &rusqlite::Connection,
1998 params: &P,
1999 birthday_height: BlockHeight,
2000 recover_until_height: Option<BlockHeight>,
2001 chain_tip_height: BlockHeight,
2002 ) -> Result<Option<Progress>, SqliteClientError>;
2003
2004 #[cfg(feature = "orchard")]
2005 fn orchard_scan_progress<P: consensus::Parameters>(
2006 &self,
2007 conn: &rusqlite::Connection,
2008 params: &P,
2009 birthday_height: BlockHeight,
2010 recover_until_height: Option<BlockHeight>,
2011 chain_tip_height: BlockHeight,
2012 ) -> Result<Option<Progress>, SqliteClientError>;
2013}
2014
2015#[derive(Debug)]
2016pub(crate) struct SubtreeProgressEstimator;
2017
2018fn estimate_tree_size<P: consensus::Parameters>(
2019 conn: &rusqlite::Connection,
2020 params: &P,
2021 shielded_protocol: ShieldedPool,
2022 pool_activation_height: BlockHeight,
2023 chain_tip_height: BlockHeight,
2024) -> Result<Option<u64>, SqliteClientError> {
2025 let TableConstants {
2026 table_prefix,
2027 shard_height,
2028 ..
2029 } = table_constants::<SqliteClientError>(shielded_protocol)?;
2030
2031 let last_scanned = block_max_scanned(conn, params)?.and_then(|last_scanned| {
2071 match shielded_protocol {
2072 ShieldedPool::Sapling => last_scanned.sapling_tree_size(),
2073 #[cfg(feature = "orchard")]
2074 ShieldedPool::Orchard => last_scanned.orchard_tree_size(),
2075 #[cfg(not(feature = "orchard"))]
2076 ShieldedPool::Orchard => None,
2077 #[cfg(feature = "orchard")]
2078 ShieldedPool::Ironwood => last_scanned.ironwood_tree_size(),
2079 #[cfg(not(feature = "orchard"))]
2080 ShieldedPool::Ironwood => None,
2081 }
2082 .map(|tree_size| (last_scanned.block_height(), u64::from(tree_size)))
2083 });
2084
2085 let last_completed_subtree = conn
2087 .query_row(
2088 &format!(
2089 "SELECT shard_index, subtree_end_height
2090 FROM {table_prefix}_tree_shards
2091 WHERE subtree_end_height IS NOT NULL
2092 ORDER BY shard_index DESC
2093 LIMIT 1"
2094 ),
2095 [],
2096 |row| {
2097 Ok((
2098 incrementalmerkletree::Address::from_parts(
2099 incrementalmerkletree::Level::new(shard_height),
2100 row.get(0)?,
2101 ),
2102 BlockHeight::from_u32(row.get(1)?),
2103 ))
2104 },
2105 )
2106 .optional()?;
2108
2109 let result = if let Some((last_completed_subtree, last_completed_subtree_end)) =
2110 last_completed_subtree
2111 {
2112 let tip_tree_size = last_scanned.and_then(|(last_scanned, last_scanned_tree_size)| {
2115 (last_scanned > last_completed_subtree_end)
2116 .then(|| {
2117 let scanned_notes = last_scanned_tree_size
2118 .saturating_sub(u64::from(last_completed_subtree.position_range_end()));
2119 let scanned_range = u64::from(last_scanned - last_completed_subtree_end);
2120 let unscanned_range = u64::from(chain_tip_height - last_scanned);
2121
2122 (scanned_notes * unscanned_range)
2123 .checked_div(scanned_range)
2124 .map(|extrapolated_unscanned_notes| {
2125 last_scanned_tree_size + extrapolated_unscanned_notes
2126 })
2127 })
2128 .flatten()
2129 });
2130
2131 if let Some(tree_size) = tip_tree_size {
2132 Some(tree_size)
2133 } else if let Some(second_to_last_completed_subtree_end) = last_completed_subtree
2134 .index()
2135 .checked_sub(1)
2136 .and_then(|subtree_index| {
2137 conn.query_row(
2138 &format!(
2139 "SELECT subtree_end_height
2140 FROM {table_prefix}_tree_shards
2141 WHERE shard_index = :shard_index"
2142 ),
2143 named_params! {":shard_index": subtree_index},
2144 |row| Ok(row.get::<_, Option<_>>(0)?.map(BlockHeight::from_u32)),
2145 )
2146 .transpose()
2147 })
2148 .transpose()?
2149 {
2150 let notes_in_complete_subtrees = u64::from(last_completed_subtree.position_range_end());
2151
2152 let subtree_notes = 1 << shard_height;
2153 let subtree_range =
2154 u64::from(last_completed_subtree_end - second_to_last_completed_subtree_end);
2155 let unscanned_range = u64::from(chain_tip_height - last_completed_subtree_end);
2156
2157 (subtree_notes * unscanned_range)
2158 .checked_div(subtree_range)
2159 .map(|extrapolated_incomplete_subtree_notes| {
2160 notes_in_complete_subtrees + extrapolated_incomplete_subtree_notes
2161 })
2162 } else {
2163 let subtree_notes = 1 << shard_height;
2166
2167 let subtree_range = u64::from(last_completed_subtree_end - pool_activation_height);
2168 let unscanned_range = u64::from(chain_tip_height - last_completed_subtree_end);
2169
2170 (subtree_notes * unscanned_range)
2171 .checked_div(subtree_range)
2172 .map(|extrapolated_incomplete_subtree_notes| {
2173 subtree_notes + extrapolated_incomplete_subtree_notes
2174 })
2175 }
2176 } else {
2177 last_scanned.and_then(|(last_scanned_height, last_scanned_tree_size)| {
2182 let subtree_range = u64::from(last_scanned_height - pool_activation_height);
2183 let unscanned_range = u64::from(chain_tip_height - last_scanned_height);
2184
2185 (last_scanned_tree_size * unscanned_range)
2186 .checked_div(subtree_range)
2187 .map(|extrapolated_incomplete_subtree_notes| {
2188 last_scanned_tree_size + extrapolated_incomplete_subtree_notes
2189 })
2190 })
2191 };
2192
2193 Ok(result)
2194}
2195
2196#[allow(clippy::too_many_arguments)]
2197fn subtree_scan_progress<P: consensus::Parameters>(
2198 conn: &rusqlite::Connection,
2199 params: &P,
2200 shielded_protocol: ShieldedPool,
2201 pool_activation_height: BlockHeight,
2202 min_birthday_height: BlockHeight,
2203 recover_until_height: Option<BlockHeight>,
2204 chain_tip_height: BlockHeight,
2205) -> Result<Option<Progress>, SqliteClientError> {
2206 let TableConstants {
2207 table_prefix,
2208 output_count_col,
2209 shard_height,
2210 ..
2211 } = table_constants::<SqliteClientError>(shielded_protocol)?;
2212
2213 let scanned_priority = priority_code(&ScanPriority::Scanned);
2221 let unscanned_filter = "AND NOT EXISTS (
2222 SELECT 1 FROM scan_queue
2223 WHERE block_range_start <= blocks.height
2224 AND blocks.height < block_range_end
2225 AND priority > :scanned_priority
2226 )";
2227
2228 let mut stmt_scanned_count_until = conn.prepare_cached(&format!(
2229 "SELECT SUM({output_count_col})
2230 FROM blocks
2231 WHERE :start_height <= height AND height < :end_height
2232 {unscanned_filter}",
2233 ))?;
2234 let mut stmt_scanned_count_from = conn.prepare_cached(&format!(
2235 "SELECT SUM({output_count_col})
2236 FROM blocks
2237 WHERE :start_height <= height
2238 {unscanned_filter}",
2239 ))?;
2240 let mut stmt_start_tree_size = conn.prepare_cached(&format!(
2241 "SELECT MAX({table_prefix}_commitment_tree_size - {output_count_col})
2242 FROM blocks
2243 WHERE height <= :start_height
2244 {unscanned_filter}",
2245 ))?;
2246 let mut stmt_start_tree_size_at = conn.prepare_cached(&format!(
2247 "SELECT {table_prefix}_commitment_tree_size - {output_count_col}
2248 FROM blocks
2249 WHERE height = :start_height
2250 {unscanned_filter}",
2251 ))?;
2252
2253 let mut get_tree_size_near = |as_of: BlockHeight| {
2257 let size_from_blocks = stmt_start_tree_size
2258 .query_row(
2259 named_params![
2260 ":start_height": u32::from(as_of),
2261 ":scanned_priority": scanned_priority,
2262 ],
2263 |row| row.get::<_, Option<u64>>(0),
2264 )
2265 .optional()?
2266 .flatten();
2267
2268 let size_from_subtree_roots = || {
2269 conn.query_row(
2270 &format!(
2271 "SELECT MIN(shard_index)
2272 FROM {table_prefix}_tree_shards
2273 WHERE subtree_end_height >= :start_height
2274 OR subtree_end_height IS NULL",
2275 ),
2276 named_params! {
2277 ":start_height": u32::from(as_of),
2278 },
2279 |row| {
2280 let min_tree_size = row
2281 .get::<_, Option<u64>>(0)?
2282 .map(|min_idx| min_idx << shard_height);
2283 Ok(min_tree_size)
2284 },
2285 )
2286 .optional()
2287 .map(|opt| opt.flatten())
2288 };
2289
2290 match size_from_blocks {
2291 Some(size) => Ok(Some(size)),
2292 None => size_from_subtree_roots(),
2293 }
2294 };
2295
2296 let birthday_size = match conn
2299 .query_row(
2300 &format!(
2301 "SELECT birthday_{table_prefix}_tree_size
2302 FROM accounts
2303 WHERE birthday_height = :birthday_height",
2304 ),
2305 named_params![":birthday_height": u32::from(min_birthday_height)],
2306 |row| row.get::<_, Option<u64>>(0),
2307 )
2308 .optional()?
2309 .flatten()
2310 {
2311 Some(tree_size) => Some(tree_size),
2312 None => get_tree_size_near(min_birthday_height)?,
2314 };
2315
2316 let tip_tree_size = match conn
2319 .query_row(
2320 &format!(
2321 "SELECT {table_prefix}_commitment_tree_size
2322 FROM blocks
2323 WHERE height = :height
2324 {unscanned_filter}",
2325 ),
2326 named_params! {
2327 ":height": u32::from(chain_tip_height),
2328 ":scanned_priority": scanned_priority,
2329 },
2330 |row| row.get::<_, Option<u64>>(0),
2331 )
2332 .optional()?
2333 .flatten()
2334 {
2335 Some(tree_size) => Some(tree_size),
2336 None => estimate_tree_size(
2337 conn,
2338 params,
2339 shielded_protocol,
2340 pool_activation_height,
2341 chain_tip_height,
2342 )?,
2343 };
2344
2345 let recover_until_size: Option<Option<u64>> = recover_until_height
2350 .map(|recover_until_height| {
2351 let size_from_blocks = stmt_start_tree_size_at
2352 .query_row(
2353 named_params![
2354 ":start_height": u32::from(recover_until_height),
2355 ":scanned_priority": scanned_priority,
2356 ],
2357 |row| row.get::<_, Option<u64>>(0),
2358 )
2359 .optional()?
2360 .flatten();
2361
2362 match size_from_blocks {
2363 Some(size) => Ok::<_, SqliteClientError>(Some(size)),
2365
2366 None if recover_until_height == chain_tip_height => Ok(tip_tree_size),
2373
2374 None => {
2378 Ok(birthday_size
2379 .zip(tip_tree_size)
2380 .and_then(|(lower_size, upper_size)| {
2381 let total_notes = upper_size.saturating_sub(lower_size);
2382 let total_range = u64::from(chain_tip_height)
2383 .saturating_sub(u64::from(min_birthday_height));
2384 let recovery_range = u64::from(recover_until_height)
2385 .saturating_sub(u64::from(min_birthday_height));
2386
2387 (total_notes * recovery_range).checked_div(total_range).map(
2388 |extrapolated_recovery_notes| {
2389 (lower_size + extrapolated_recovery_notes).min(upper_size)
2390 },
2391 )
2392 }))
2393 }
2394 }
2395 })
2396 .transpose()?;
2397
2398 let recovered_count = recover_until_height
2400 .map(|end_height| {
2401 stmt_scanned_count_until.query_row(
2402 named_params! {
2403 ":start_height": u32::from(min_birthday_height),
2404 ":end_height": u32::from(end_height),
2405 ":scanned_priority": scanned_priority,
2406 },
2407 |row| row.get::<_, Option<u64>>(0),
2408 )
2409 })
2410 .transpose()?;
2411
2412 let recover = recovered_count
2413 .zip(recover_until_size)
2414 .map(|(recovered, end_size)| {
2415 birthday_size.zip(end_size).map(|(start_size, end_size)| {
2416 Ratio::new(recovered.unwrap_or(0), end_size.saturating_sub(start_size))
2417 })
2418 })
2419 .unwrap_or_else(|| Some(Ratio::new(0, 0)));
2423
2424 let scan = {
2425 let scanned_count = stmt_scanned_count_from.query_row(
2428 named_params![
2429 ":start_height": u32::from(recover_until_height.unwrap_or(min_birthday_height)),
2430 ":scanned_priority": scanned_priority,
2431 ],
2432 |row| row.get::<_, Option<u64>>(0),
2433 )?;
2434
2435 recover_until_size
2436 .unwrap_or(birthday_size)
2437 .zip(tip_tree_size)
2438 .map(|(start_size, tip_tree_size)| {
2439 Ratio::new(
2440 scanned_count.unwrap_or(0),
2441 tip_tree_size.saturating_sub(start_size),
2442 )
2443 })
2444 };
2445
2446 Ok(scan.map(|scan| Progress::new(scan, recover)))
2447}
2448
2449impl ProgressEstimator for SubtreeProgressEstimator {
2450 #[tracing::instrument(skip(conn, params))]
2451 fn sapling_scan_progress<P: consensus::Parameters>(
2452 &self,
2453 conn: &rusqlite::Connection,
2454 params: &P,
2455 birthday_height: BlockHeight,
2456 recover_until_height: Option<BlockHeight>,
2457 chain_tip_height: BlockHeight,
2458 ) -> Result<Option<Progress>, SqliteClientError> {
2459 let sapling_activation_height = match params.activation_height(NetworkUpgrade::Sapling) {
2460 Some(h) => h,
2461 None => return Ok(None),
2462 };
2463
2464 subtree_scan_progress(
2465 conn,
2466 params,
2467 ShieldedPool::Sapling,
2468 sapling_activation_height,
2469 birthday_height,
2470 recover_until_height,
2471 chain_tip_height,
2472 )
2473 }
2474
2475 #[cfg(feature = "orchard")]
2476 #[tracing::instrument(skip(conn, params))]
2477 fn orchard_scan_progress<P: consensus::Parameters>(
2478 &self,
2479 conn: &rusqlite::Connection,
2480 params: &P,
2481 birthday_height: BlockHeight,
2482 recover_until_height: Option<BlockHeight>,
2483 chain_tip_height: BlockHeight,
2484 ) -> Result<Option<Progress>, SqliteClientError> {
2485 let nu5_activation_height = match params.activation_height(NetworkUpgrade::Nu5) {
2486 Some(h) => h,
2487 None => return Ok(None),
2488 };
2489
2490 subtree_scan_progress(
2491 conn,
2492 params,
2493 ShieldedPool::Orchard,
2494 nu5_activation_height,
2495 birthday_height,
2496 recover_until_height,
2497 chain_tip_height,
2498 )
2499 }
2500}
2501
2502fn next_subtree_index<H: HashSer, const SHARD_HEIGHT: u8>(
2503 tx: &rusqlite::Transaction,
2504 table_prefix: &'static str,
2505) -> Result<u64, SqliteClientError> {
2506 let shard_store = SqliteShardStore::<_, H, SHARD_HEIGHT>::from_connection(tx, table_prefix)?;
2507
2508 let roots = shard_store
2511 .get_shard_roots()
2512 .map_err(ShardTreeError::Storage)?;
2513 Ok(roots
2514 .iter()
2515 .rev()
2516 .nth(1)
2517 .map(|addr| addr.index())
2518 .unwrap_or(0))
2519}
2520
2521#[tracing::instrument(skip(tx, params, progress))]
2526pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
2527 tx: &rusqlite::Transaction,
2528 params: &P,
2529 confirmations_policy: ConfirmationsPolicy,
2530 progress: &impl ProgressEstimator,
2531) -> Result<Option<WalletSummary<AccountUuid>>, SqliteClientError> {
2532 let chain_tip_height = match chain_tip_height(tx)? {
2533 Some(h) => h,
2534 None => {
2535 return Ok(None);
2536 }
2537 };
2538
2539 let birthday_height = match wallet_birthday(tx)? {
2540 Some(h) => h,
2541 None => {
2542 return Ok(None);
2543 }
2544 };
2545
2546 let recover_until_height = recover_until_height(tx)?;
2547 let fully_scanned_height = block_fully_scanned(tx, params)?.map(|m| m.block_height());
2548 let target_height = TargetHeight::from(chain_tip_height + 1);
2549 let anchor_height = get_anchor_height(tx, target_height, confirmations_policy.trusted())?;
2550
2551 let sapling_progress = progress.sapling_scan_progress(
2552 tx,
2553 params,
2554 birthday_height,
2555 recover_until_height,
2556 chain_tip_height,
2557 )?;
2558
2559 #[cfg(feature = "orchard")]
2560 let orchard_progress = progress.orchard_scan_progress(
2561 tx,
2562 params,
2563 birthday_height,
2564 recover_until_height,
2565 chain_tip_height,
2566 )?;
2567 #[cfg(not(feature = "orchard"))]
2568 let orchard_progress: Option<Progress> = None;
2569
2570 let progress = sapling_progress
2572 .as_ref()
2573 .zip(orchard_progress.as_ref())
2574 .map(|(s, o)| {
2575 Progress::new(
2576 Ratio::new(
2577 s.scan().numerator() + o.scan().numerator(),
2578 s.scan().denominator() + o.scan().denominator(),
2579 ),
2580 s.recovery()
2581 .zip(o.recovery())
2582 .map(|(s, o)| {
2583 Ratio::new(
2584 s.numerator() + o.numerator(),
2585 s.denominator() + o.denominator(),
2586 )
2587 })
2588 .or_else(|| s.recovery())
2589 .or_else(|| o.recovery()),
2590 )
2591 })
2592 .or(sapling_progress)
2593 .or(orchard_progress);
2594
2595 let progress = match progress {
2596 Some(p) => p,
2597 None => return Ok(None),
2598 };
2599
2600 let mut stmt_accounts = tx.prepare_cached("SELECT uuid FROM accounts")?;
2601 let mut account_balances = stmt_accounts
2602 .query([])?
2603 .and_then(|row| {
2604 Ok::<_, SqliteClientError>((AccountUuid(row.get::<_, Uuid>(0)?), AccountBalance::ZERO))
2605 })
2606 .collect::<Result<HashMap<AccountUuid, AccountBalance>, _>>()?;
2607
2608 fn with_pool_balances<F>(
2609 tx: &rusqlite::Transaction,
2610 target_height: TargetHeight,
2611 anchor_height: Option<BlockHeight>,
2612 confirmations_policy: ConfirmationsPolicy,
2613 account_balances: &mut HashMap<AccountUuid, AccountBalance>,
2614 protocol: ShieldedPool,
2615 with_pool_balance: F,
2616 ) -> Result<(), SqliteClientError>
2617 where
2618 F: Fn(
2619 &mut AccountBalance,
2620 Zatoshis,
2621 Zatoshis,
2622 Zatoshis,
2623 Zatoshis,
2624 Zatoshis,
2625 ) -> Result<(), SqliteClientError>,
2626 {
2627 let TableConstants { table_prefix, .. } = table_constants::<SqliteClientError>(protocol)?;
2628
2629 #[tracing::instrument(skip_all)]
2632 fn is_any_spendable(
2633 conn: &rusqlite::Connection,
2634 anchor_height: BlockHeight,
2635 table_prefix: &'static str,
2636 ) -> Result<bool, SqliteClientError> {
2637 conn.query_row(
2638 &format!(
2639 "SELECT NOT EXISTS(
2640 SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges
2641 WHERE :anchor_height
2642 BETWEEN subtree_start_height
2643 AND IFNULL(subtree_end_height, :anchor_height)
2644 AND block_range_start <= :anchor_height
2645 )"
2646 ),
2647 named_params![":anchor_height": u32::from(anchor_height)],
2648 |row| row.get::<_, bool>(0),
2649 )
2650 .map_err(|e| e.into())
2651 }
2652
2653 let trusted_height =
2654 target_height.saturating_sub(u32::from(confirmations_policy.trusted()));
2655
2656 let any_spendable =
2657 anchor_height.map_or(Ok(false), |h| is_any_spendable(tx, h, table_prefix))?;
2658
2659 let mut stmt_select_notes = tx.prepare_cached(&format!(
2660 "SELECT accounts.uuid, rn.id, rn.value, rn.is_change, rn.recipient_key_scope,
2661 scan_state.max_priority,
2662 rn.witness_stabilized,
2663 t.mined_height,
2664 IFNULL(t.trust_status, 0) AS trust_status,
2665 MAX(tt.mined_height) AS max_shielding_input_height,
2666 MIN(IFNULL(tt.trust_status, 0)) AS min_shielding_input_trust,
2667 rn.lock_expiry_height
2668 FROM {table_prefix}_received_notes rn
2669 INNER JOIN accounts ON accounts.id = rn.account_id
2670 INNER JOIN transactions t ON t.id_tx = rn.transaction_id
2671 LEFT OUTER JOIN v_{table_prefix}_shards_scan_state scan_state
2672 ON rn.commitment_tree_position >= scan_state.start_position
2673 AND rn.commitment_tree_position < scan_state.end_position_exclusive
2674 LEFT OUTER JOIN transparent_received_output_spends ros
2675 ON ros.transaction_id = t.id_tx
2676 LEFT OUTER JOIN transparent_received_outputs tro
2677 ON tro.id = ros.transparent_received_output_id
2678 AND tro.account_id = accounts.id
2679 LEFT OUTER JOIN transactions tt
2680 ON tt.id_tx = tro.transaction_id
2681 WHERE ({}) -- the transaction is unexpired
2682 AND rn.id NOT IN ({}) -- and the received note is unspent
2683 GROUP BY rn.id",
2684 common::tx_unexpired_condition("t"),
2685 common::spent_notes_clause(table_prefix),
2686 ))?;
2687
2688 let mut rows = stmt_select_notes.query(named_params![
2689 ":target_height": u32::from(target_height),
2690 ])?;
2691 while let Some(row) = rows.next()? {
2692 let account = AccountUuid(row.get::<_, Uuid>("uuid")?);
2693
2694 let value_raw = row.get::<_, i64>("value")?;
2695 let value = Zatoshis::from_nonnegative_i64(value_raw).map_err(|_| {
2696 SqliteClientError::CorruptedData(format!(
2697 "Negative received note value: {value_raw}"
2698 ))
2699 })?;
2700
2701 let is_change = row.get::<_, bool>("is_change")?;
2702
2703 let recipient_key_scope = row
2704 .get::<_, Option<i64>>("recipient_key_scope")?
2705 .map(KeyScope::decode)
2706 .transpose()?;
2707
2708 let max_priority_raw = row.get::<_, Option<i64>>("max_priority")?;
2712 let max_priority = max_priority_raw.map_or_else(
2713 || Ok(ScanPriority::ChainTip),
2714 |raw| {
2715 parse_priority_code(raw).ok_or_else(|| {
2716 SqliteClientError::CorruptedData(format!(
2717 "Priority code {raw} not recognized."
2718 ))
2719 })
2720 },
2721 )?;
2722
2723 let received_height = row
2724 .get::<_, Option<u32>>("mined_height")?
2725 .map(BlockHeight::from);
2726
2727 let tx_trusted = row.get::<_, bool>("trust_status")?;
2728
2729 let max_shielding_input_height = row
2730 .get::<_, Option<u32>>("max_shielding_input_height")?
2731 .map(BlockHeight::from);
2732
2733 let tx_shielding_inputs_trusted = row.get::<_, bool>("min_shielding_input_trust")?;
2734
2735 let witness_stabilized = row.get::<_, bool>("witness_stabilized")?;
2736
2737 let is_locked = locking::is_locked_at(
2738 row.get::<_, Option<u32>>("lock_expiry_height")?,
2739 target_height,
2740 );
2741
2742 let is_spendable = witness_stabilized
2751 || (any_spendable
2752 && max_priority <= ScanPriority::Scanned
2753 && confirmations_policy.confirmations_until_spendable(
2754 target_height,
2755 PoolType::Shielded(protocol),
2756 recipient_key_scope.and_then(|k| zip32::Scope::try_from(k).ok()),
2757 received_height,
2758 tx_trusted,
2759 max_shielding_input_height,
2760 tx_shielding_inputs_trusted,
2761 ) == 0);
2762
2763 let is_pending_change =
2764 is_change && received_height.iter().all(|h| h > &trusted_height);
2765
2766 let (
2767 spendable_value,
2768 locked_value,
2769 change_pending_confirmation,
2770 value_pending_spendability,
2771 uneconomic_value,
2772 ) = {
2773 let zero = Zatoshis::ZERO;
2774 if value <= zip317::MARGINAL_FEE {
2775 (zero, zero, zero, zero, value)
2776 } else if is_spendable && is_locked {
2777 (zero, value, zero, zero, zero)
2782 } else if is_spendable {
2783 (value, zero, zero, zero, zero)
2784 } else if is_pending_change {
2785 (zero, zero, value, zero, zero)
2786 } else {
2787 (zero, zero, zero, value, zero)
2788 }
2789 };
2790
2791 if let Some(balances) = account_balances.get_mut(&account) {
2792 with_pool_balance(
2793 balances,
2794 spendable_value,
2795 locked_value,
2796 change_pending_confirmation,
2797 value_pending_spendability,
2798 uneconomic_value,
2799 )?;
2800 }
2801 }
2802 Ok(())
2803 }
2804
2805 #[cfg(feature = "orchard")]
2806 {
2807 let orchard_trace = tracing::info_span!("orchard_balances").entered();
2808 with_pool_balances(
2809 tx,
2810 target_height,
2811 anchor_height,
2812 confirmations_policy,
2813 &mut account_balances,
2814 ShieldedPool::Orchard,
2815 |balances,
2816 spendable_value,
2817 locked_value,
2818 change_pending_confirmation,
2819 value_pending_spendability,
2820 uneconomic_value| {
2821 balances.with_orchard_balance_mut::<_, SqliteClientError>(|bal| {
2822 bal.add_spendable_value(spendable_value)?;
2823 bal.add_locked_value(locked_value)?;
2824 bal.add_pending_change_value(change_pending_confirmation)?;
2825 bal.add_pending_spendable_value(value_pending_spendability)?;
2826 bal.add_uneconomic_value(uneconomic_value)?;
2827 Ok(())
2828 })
2829 },
2830 )?;
2831 drop(orchard_trace);
2832 }
2833
2834 #[cfg(feature = "orchard")]
2835 {
2836 let ironwood_trace = tracing::info_span!("ironwood_balances").entered();
2837 with_pool_balances(
2838 tx,
2839 target_height,
2840 anchor_height,
2841 confirmations_policy,
2842 &mut account_balances,
2843 ShieldedPool::Ironwood,
2844 |balances,
2845 spendable_value,
2846 locked_value,
2847 change_pending_confirmation,
2848 value_pending_spendability,
2849 uneconomic_value| {
2850 balances.with_ironwood_balance_mut::<_, SqliteClientError>(|bal| {
2851 bal.add_spendable_value(spendable_value)?;
2852 bal.add_locked_value(locked_value)?;
2853 bal.add_pending_change_value(change_pending_confirmation)?;
2854 bal.add_pending_spendable_value(value_pending_spendability)?;
2855 bal.add_uneconomic_value(uneconomic_value)?;
2856 Ok(())
2857 })
2858 },
2859 )?;
2860 drop(ironwood_trace);
2861 }
2862
2863 let sapling_trace = tracing::info_span!("sapling_balances").entered();
2864 with_pool_balances(
2865 tx,
2866 target_height,
2867 anchor_height,
2868 confirmations_policy,
2869 &mut account_balances,
2870 ShieldedPool::Sapling,
2871 |balances,
2872 spendable_value,
2873 locked_value,
2874 change_pending_confirmation,
2875 value_pending_spendability,
2876 uneconomic_value| {
2877 balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| {
2878 bal.add_spendable_value(spendable_value)?;
2879 bal.add_locked_value(locked_value)?;
2880 bal.add_pending_change_value(change_pending_confirmation)?;
2881 bal.add_pending_spendable_value(value_pending_spendability)?;
2882 bal.add_uneconomic_value(uneconomic_value)?;
2883 Ok(())
2884 })
2885 },
2886 )?;
2887 drop(sapling_trace);
2888
2889 #[cfg(feature = "transparent-inputs")]
2890 transparent::add_transparent_account_balances(
2891 tx,
2892 target_height,
2893 confirmations_policy,
2894 &mut account_balances,
2895 )?;
2896
2897 let next_sapling_subtree_index = next_subtree_index::<::sapling::Node, SAPLING_SHARD_HEIGHT>(
2901 tx,
2902 crate::SAPLING_TABLES_PREFIX,
2903 )?;
2904
2905 #[cfg(feature = "orchard")]
2906 let next_orchard_subtree_index = next_subtree_index::<
2907 ::orchard::tree::MerkleHashOrchard,
2908 ORCHARD_SHARD_HEIGHT,
2909 >(tx, crate::ORCHARD_TABLES_PREFIX)?;
2910
2911 #[cfg(feature = "orchard")]
2912 let next_ironwood_subtree_index = next_subtree_index::<
2913 ::orchard::tree::MerkleHashOrchard,
2914 ORCHARD_SHARD_HEIGHT,
2915 >(tx, crate::IRONWOOD_TABLES_PREFIX)?;
2916
2917 let summary = WalletSummary::new(
2918 account_balances,
2919 chain_tip_height,
2920 fully_scanned_height.unwrap_or(birthday_height - 1),
2921 progress,
2922 next_sapling_subtree_index,
2923 #[cfg(feature = "orchard")]
2924 next_orchard_subtree_index,
2925 #[cfg(feature = "orchard")]
2926 next_ironwood_subtree_index,
2927 );
2928
2929 Ok(Some(summary))
2930}
2931
2932pub(crate) fn get_received_memo(
2934 conn: &rusqlite::Connection,
2935 note_id: NoteId,
2936) -> Result<Option<Memo>, SqliteClientError> {
2937 let TableConstants {
2938 table_prefix,
2939 output_index_col,
2940 ..
2941 } = table_constants::<SqliteClientError>(note_id.protocol())?;
2942
2943 let memo_bytes = conn
2944 .query_row(
2945 &format!(
2946 "SELECT memo FROM {table_prefix}_received_notes
2947 JOIN transactions ON transactions.id_tx = {table_prefix}_received_notes.transaction_id
2948 WHERE transactions.txid = :txid
2949 AND {table_prefix}_received_notes.{output_index_col} = :output_index"
2950 ),
2951 named_params![
2952 ":txid": note_id.txid().as_ref(),
2953 ":output_index": note_id.output_index()
2954 ],
2955 |row| row.get::<_, Option<Vec<u8>>>(0),
2956 )
2957 .optional()?
2958 .flatten();
2959
2960 let memo = memo_bytes
2961 .map(|b| MemoBytes::from_bytes(&b).and_then(Memo::try_from))
2962 .transpose()?;
2963
2964 Ok(memo)
2965}
2966
2967fn parse_tx<P: consensus::Parameters>(
2968 params: &P,
2969 tx_bytes: &[u8],
2970 block_height: Option<BlockHeight>,
2971 expiry_height: Option<BlockHeight>,
2972) -> Result<(BlockHeight, Transaction), SqliteClientError> {
2973 if let Some(height) =
2983 block_height.or_else(|| expiry_height.filter(|h| h > &BlockHeight::from(0)))
2984 {
2985 Transaction::read(tx_bytes, BranchId::for_height(params, height))
2986 .map(|t| (height, t))
2987 .map_err(SqliteClientError::from)
2988 } else {
2989 let tx_data = Transaction::read(tx_bytes, BranchId::Sprout)
2990 .map_err(SqliteClientError::from)?
2991 .into_data();
2992
2993 let expiry_height = tx_data.expiry_height();
2994 if expiry_height > BlockHeight::from(0) {
2995 TransactionData::from_parts(
2996 tx_data.version(),
2997 BranchId::for_height(params, expiry_height),
2998 tx_data.lock_time(),
2999 expiry_height,
3000 #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
3001 tx_data.zip233_amount(),
3002 tx_data.transparent_bundle().cloned(),
3003 tx_data.sprout_bundle().cloned(),
3004 tx_data.sapling_bundle().cloned(),
3005 tx_data.orchard_bundle().cloned(),
3006 )
3007 .freeze()
3008 .map(|t| (expiry_height, t))
3009 .map_err(SqliteClientError::from)
3010 } else {
3011 Err(SqliteClientError::CorruptedData(
3012 "Consensus branch ID not known, cannot parse this transaction until it is mined"
3013 .to_string(),
3014 ))
3015 }
3016 }
3017}
3018
3019pub(crate) fn get_transaction<P: Parameters>(
3026 conn: &rusqlite::Connection,
3027 params: &P,
3028 txid: TxId,
3029) -> Result<Option<(BlockHeight, Transaction)>, SqliteClientError> {
3030 conn.query_row(
3031 "SELECT raw, mined_height, expiry_height FROM transactions
3032 WHERE txid = ?",
3033 [txid.as_ref()],
3034 |row| {
3035 let h: Option<u32> = row.get(1)?;
3036 let expiry: Option<u32> = row.get(2)?;
3037 Ok((
3038 row.get::<_, Option<Vec<u8>>>(0)?,
3039 h.map(BlockHeight::from),
3040 expiry.map(BlockHeight::from),
3041 ))
3042 },
3043 )
3044 .optional()?
3045 .and_then(|(t_opt, b, e)| t_opt.as_ref().map(|t| parse_tx(params, t, b, e)))
3046 .transpose()
3047}
3048
3049pub(crate) fn get_sent_memo(
3051 conn: &rusqlite::Connection,
3052 note_id: NoteId,
3053) -> Result<Option<Memo>, SqliteClientError> {
3054 let memo_bytes: Option<Vec<_>> = conn
3055 .query_row(
3056 "SELECT memo FROM sent_notes
3057 JOIN transactions ON transactions.id_tx = sent_notes.transaction_id
3058 WHERE transactions.txid = :txid
3059 AND sent_notes.output_pool = :pool_code
3060 AND sent_notes.output_index = :output_index",
3061 named_params![
3062 ":txid": note_id.txid().as_ref(),
3063 ":pool_code": pool_code(PoolType::Shielded(note_id.protocol())),
3064 ":output_index": note_id.output_index()
3065 ],
3066 |row| row.get(0),
3067 )
3068 .optional()?
3069 .flatten();
3070
3071 memo_bytes
3072 .map(|b| {
3073 MemoBytes::from_bytes(&b)
3074 .and_then(Memo::try_from)
3075 .map_err(SqliteClientError::from)
3076 })
3077 .transpose()
3078}
3079
3080pub(crate) fn wallet_birthday(
3086 conn: &rusqlite::Connection,
3087) -> Result<Option<BlockHeight>, rusqlite::Error> {
3088 conn.query_row(
3089 "SELECT MIN(birthday_height) AS wallet_birthday FROM accounts",
3090 [],
3091 |row| {
3092 row.get::<_, Option<u32>>(0)
3093 .map(|opt| opt.map(BlockHeight::from))
3094 },
3095 )
3096}
3097
3098pub(crate) fn wallet_recover_until(
3100 conn: &rusqlite::Connection,
3101) -> Result<Option<BlockHeight>, rusqlite::Error> {
3102 conn.query_row(
3103 "SELECT MAX(recover_until_height) AS wallet_recover_until FROM accounts",
3104 [],
3105 |row| {
3106 row.get::<_, Option<u32>>(0)
3107 .map(|opt| opt.map(BlockHeight::from))
3108 },
3109 )
3110}
3111
3112pub(crate) fn account_birthday(
3113 conn: &rusqlite::Connection,
3114 account_uuid: AccountUuid,
3115) -> Result<BlockHeight, SqliteClientError> {
3116 conn.query_row(
3117 "SELECT birthday_height
3118 FROM accounts
3119 WHERE uuid = :account_uuid",
3120 named_params![":account_uuid": account_uuid.0],
3121 |row| row.get::<_, u32>(0).map(BlockHeight::from),
3122 )
3123 .optional()
3124 .map_err(SqliteClientError::from)
3125 .and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown))
3126}
3127
3128#[cfg(feature = "transparent-inputs")]
3129pub(crate) fn account_birthday_internal(
3130 conn: &rusqlite::Connection,
3131 account_ref: AccountRef,
3132) -> Result<BlockHeight, SqliteClientError> {
3133 conn.query_row(
3134 "SELECT birthday_height
3135 FROM accounts
3136 WHERE id = :account_ref",
3137 named_params![":account_ref": account_ref.0],
3138 |row| row.get::<_, u32>(0).map(BlockHeight::from),
3139 )
3140 .optional()
3141 .map_err(SqliteClientError::from)
3142 .and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown))
3143}
3144
3145pub(crate) fn recover_until_height(
3147 conn: &rusqlite::Connection,
3148) -> Result<Option<BlockHeight>, rusqlite::Error> {
3149 conn.query_row(
3150 "SELECT MAX(recover_until_height) FROM accounts",
3151 [],
3152 |row| {
3153 row.get::<_, Option<u32>>(0)
3154 .map(|opt| opt.map(BlockHeight::from))
3155 },
3156 )
3157}
3158
3159pub(crate) fn block_height_extrema(
3161 conn: &rusqlite::Connection,
3162) -> Result<Option<RangeInclusive<BlockHeight>>, rusqlite::Error> {
3163 conn.query_row("SELECT MIN(height), MAX(height) FROM blocks", [], |row| {
3164 let min_height: Option<u32> = row.get(0)?;
3165 let max_height: Option<u32> = row.get(1)?;
3166 Ok(min_height
3167 .zip(max_height)
3168 .map(|(min, max)| RangeInclusive::new(min.into(), max.into())))
3169 })
3170}
3171
3172pub(crate) fn get_account_ref(
3173 conn: &rusqlite::Connection,
3174 account_uuid: AccountUuid,
3175) -> Result<AccountRef, SqliteClientError> {
3176 conn.query_row(
3177 "SELECT id FROM accounts WHERE uuid = :account_uuid",
3178 named_params! {":account_uuid": account_uuid.0},
3179 |row| row.get("id").map(AccountRef),
3180 )
3181 .optional()?
3182 .ok_or(SqliteClientError::AccountUnknown)
3183}
3184
3185pub(crate) fn anchor_computable(
3190 conn: &rusqlite::Connection,
3191 protocol: ShieldedPool,
3192 height: BlockHeight,
3193) -> Result<bool, SqliteClientError> {
3194 let TableConstants { table_prefix, .. } =
3195 common::table_constants::<SqliteClientError>(protocol)?;
3196 conn.query_row(
3197 &format!(
3198 "SELECT EXISTS (
3199 SELECT 1 FROM {table_prefix}_tree_checkpoints WHERE checkpoint_id = :height
3200 )"
3201 ),
3202 named_params![":height": u32::from(height)],
3203 |row| row.get(0),
3204 )
3205 .map_err(SqliteClientError::from)
3206}
3207
3208pub(crate) fn chain_tip_height(
3210 conn: &rusqlite::Connection,
3211) -> Result<Option<BlockHeight>, rusqlite::Error> {
3212 conn.query_row("SELECT MAX(block_range_end) FROM scan_queue", [], |row| {
3213 let max_height: Option<u32> = row.get(0)?;
3214
3215 Ok(max_height.map(|h| BlockHeight::from(h.saturating_sub(1))))
3218 })
3219}
3220
3221pub(crate) fn mempool_height(
3222 conn: &rusqlite::Connection,
3223) -> Result<Option<TargetHeight>, rusqlite::Error> {
3224 Ok(chain_tip_height(conn)?.map(|h| TargetHeight::from(h + 1)))
3225}
3226
3227pub(crate) fn get_anchor_height(
3228 conn: &rusqlite::Connection,
3229 target_height: TargetHeight,
3230 min_confirmations: NonZeroU32,
3231) -> Result<Option<BlockHeight>, SqliteClientError> {
3232 let sapling_anchor_height = get_max_checkpointed_height(
3233 conn,
3234 ShieldedPool::Sapling,
3235 target_height,
3236 min_confirmations,
3237 )?;
3238
3239 #[cfg(feature = "orchard")]
3240 let orchard_anchor_height = get_max_checkpointed_height(
3241 conn,
3242 ShieldedPool::Orchard,
3243 target_height,
3244 min_confirmations,
3245 )?;
3246
3247 #[cfg(not(feature = "orchard"))]
3248 let orchard_anchor_height: Option<BlockHeight> = None;
3249
3250 Ok(sapling_anchor_height
3251 .zip(orchard_anchor_height)
3252 .map(|(s, o)| std::cmp::min(s, o))
3253 .or(sapling_anchor_height)
3254 .or(orchard_anchor_height))
3255}
3256
3257pub(crate) fn get_target_and_anchor_heights(
3258 conn: &rusqlite::Connection,
3259 min_confirmations: NonZeroU32,
3260) -> Result<Option<(TargetHeight, BlockHeight)>, SqliteClientError> {
3261 match mempool_height(conn)? {
3262 Some(target_height) => {
3263 let anchor_height = get_anchor_height(conn, target_height, min_confirmations)?;
3264
3265 Ok(anchor_height.map(|h| (target_height, h)))
3266 }
3267 None => Ok(None),
3268 }
3269}
3270
3271type BlockMetadataRow = (
3275 BlockHeight,
3276 Vec<u8>,
3277 Option<u32>,
3278 Vec<u8>,
3279 Option<u32>,
3280 Option<u32>,
3281);
3282
3283fn parse_block_metadata<P: consensus::Parameters>(
3284 _params: &P,
3285 row: BlockMetadataRow,
3286) -> Result<BlockMetadata, SqliteClientError> {
3287 let (
3288 block_height,
3289 hash_data,
3290 sapling_tree_size_opt,
3291 sapling_tree,
3292 _orchard_tree_size_opt,
3293 _ironwood_tree_size_opt,
3294 ) = row;
3295 let sapling_tree_size = sapling_tree_size_opt.map_or_else(|| {
3296 if sapling_tree == BLOCK_SAPLING_FRONTIER_ABSENT {
3297 Err(SqliteClientError::CorruptedData("One of either the Sapling tree size or the legacy Sapling commitment tree must be present.".to_owned()))
3298 } else {
3299 read_commitment_tree::<
3301 ::sapling::Node,
3302 _,
3303 { ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
3304 >(Cursor::new(sapling_tree))
3305 .map(|tree| tree.size().try_into().unwrap())
3306 .map_err(SqliteClientError::from)
3307 }
3308 }, Ok)?;
3309
3310 let block_hash = BlockHash::try_from_slice(&hash_data).ok_or_else(|| {
3311 SqliteClientError::from(io::Error::new(
3312 io::ErrorKind::InvalidData,
3313 format!("Invalid block hash length: {}", hash_data.len()),
3314 ))
3315 })?;
3316
3317 Ok(BlockMetadata::from_parts(
3318 block_height,
3319 block_hash,
3320 Some(sapling_tree_size),
3321 #[cfg(feature = "orchard")]
3322 if _params
3323 .activation_height(NetworkUpgrade::Nu5)
3324 .is_some_and(|nu5_activation| block_height >= nu5_activation)
3325 {
3326 _orchard_tree_size_opt
3327 } else {
3328 Some(0)
3329 },
3330 #[cfg(feature = "orchard")]
3331 if _params
3332 .activation_height(NetworkUpgrade::Nu6_3)
3333 .is_some_and(|nu6_3_activation| block_height >= nu6_3_activation)
3334 {
3335 _ironwood_tree_size_opt
3336 } else {
3337 Some(0)
3338 },
3339 ))
3340}
3341
3342#[tracing::instrument(skip(conn, params))]
3343pub(crate) fn block_metadata<P: consensus::Parameters>(
3344 conn: &rusqlite::Connection,
3345 params: &P,
3346 block_height: BlockHeight,
3347) -> Result<Option<BlockMetadata>, SqliteClientError> {
3348 conn.query_row(
3349 "SELECT height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size, ironwood_commitment_tree_size
3350 FROM blocks
3351 WHERE height = :block_height",
3352 named_params![":block_height": u32::from(block_height)],
3353 |row| {
3354 let height: u32 = row.get(0)?;
3355 let block_hash: Vec<u8> = row.get(1)?;
3356 let sapling_tree_size: Option<u32> = row.get(2)?;
3357 let sapling_tree: Vec<u8> = row.get(3)?;
3358 let orchard_tree_size: Option<u32> = row.get(4)?;
3359 let ironwood_tree_size: Option<u32> = row.get(5)?;
3360 Ok((
3361 BlockHeight::from(height),
3362 block_hash,
3363 sapling_tree_size,
3364 sapling_tree,
3365 orchard_tree_size,
3366 ironwood_tree_size,
3367 ))
3368 },
3369 )
3370 .optional()
3371 .map_err(SqliteClientError::from)
3372 .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose())
3373}
3374
3375pub(crate) fn fully_scanned_height(
3381 conn: &rusqlite::Connection,
3382) -> Result<Option<BlockHeight>, rusqlite::Error> {
3383 let Some(birthday_height) = wallet_birthday(conn)? else {
3384 return Ok(None);
3385 };
3386 let calc_fully_scanned_height = |row: &rusqlite::Row| {
3401 let block_range_start = BlockHeight::from_u32(row.get(0)?);
3402 let block_range_end = BlockHeight::from_u32(row.get(1)?);
3403
3404 Ok(if block_range_start <= birthday_height {
3409 Some(block_range_end - 1)
3411 } else {
3412 None
3413 })
3414 };
3415 Ok(conn
3416 .query_row(
3417 "SELECT block_range_start, block_range_end
3418 FROM scan_queue
3419 WHERE priority = :priority
3420 ORDER BY block_range_start ASC
3421 LIMIT 1",
3422 named_params![":priority": priority_code(&ScanPriority::Scanned)],
3423 calc_fully_scanned_height,
3424 )
3425 .optional()?
3426 .flatten())
3427}
3428
3429#[tracing::instrument(skip_all)]
3430pub(crate) fn block_fully_scanned<P: consensus::Parameters>(
3431 conn: &rusqlite::Connection,
3432 params: &P,
3433) -> Result<Option<BlockMetadata>, SqliteClientError> {
3434 match fully_scanned_height(conn)? {
3435 Some(height) => block_metadata(conn, params, height),
3436 None => Ok(None),
3437 }
3438}
3439
3440pub(crate) fn block_max_scanned<P: consensus::Parameters>(
3441 conn: &rusqlite::Connection,
3442 params: &P,
3443) -> Result<Option<BlockMetadata>, SqliteClientError> {
3444 conn.query_row(
3445 "SELECT blocks.height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size, ironwood_commitment_tree_size
3446 FROM blocks
3447 JOIN (SELECT MAX(height) AS height FROM blocks) blocks_max
3448 ON blocks.height = blocks_max.height",
3449 [],
3450 |row| {
3451 let height: u32 = row.get(0)?;
3452 let block_hash: Vec<u8> = row.get(1)?;
3453 let sapling_tree_size: Option<u32> = row.get(2)?;
3454 let sapling_tree: Vec<u8> = row.get(3)?;
3455 let orchard_tree_size: Option<u32> = row.get(4)?;
3456 let ironwood_tree_size: Option<u32> = row.get(5)?;
3457 Ok((
3458 BlockHeight::from(height),
3459 block_hash,
3460 sapling_tree_size,
3461 sapling_tree,
3462 orchard_tree_size,
3463 ironwood_tree_size,
3464 ))
3465 },
3466 )
3467 .optional()
3468 .map_err(SqliteClientError::from)
3469 .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose())
3470}
3471
3472pub(crate) fn get_tx_height(
3475 conn: &rusqlite::Connection,
3476 txid: TxId,
3477) -> Result<Option<BlockHeight>, SqliteClientError> {
3478 let chain_tip_height = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
3479
3480 let tx_height = conn
3481 .query_row(
3482 "SELECT mined_height FROM transactions WHERE txid = ?",
3483 [txid.as_ref()],
3484 |row| Ok(row.get::<_, Option<u32>>(0)?.map(BlockHeight::from)),
3485 )
3486 .optional()
3487 .map(|opt| opt.flatten())?;
3488
3489 Ok(tx_height.filter(|h| h <= &chain_tip_height))
3490}
3491
3492pub(crate) fn get_block_hash(
3495 conn: &rusqlite::Connection,
3496 block_height: BlockHeight,
3497) -> Result<Option<BlockHash>, rusqlite::Error> {
3498 conn.query_row(
3499 "SELECT hash FROM blocks WHERE height = ?",
3500 [u32::from(block_height)],
3501 |row| {
3502 let row_data = row.get::<_, Vec<_>>(0)?;
3503 Ok(BlockHash::from_slice(&row_data))
3504 },
3505 )
3506 .optional()
3507}
3508
3509pub(crate) fn get_max_height_hash(
3510 conn: &rusqlite::Connection,
3511) -> Result<Option<(BlockHeight, BlockHash)>, rusqlite::Error> {
3512 conn.query_row(
3513 "SELECT height, hash FROM blocks ORDER BY height DESC LIMIT 1",
3514 [],
3515 |row| {
3516 let height = row.get::<_, u32>(0).map(BlockHeight::from)?;
3517 let row_data = row.get::<_, Vec<_>>(1)?;
3518 Ok((height, BlockHash::from_slice(&row_data)))
3519 },
3520 )
3521 .optional()
3522}
3523
3524pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters>(
3525 conn: &rusqlite::Transaction,
3526 params: &P,
3527 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
3528 sent_tx: &SentTransaction<AccountUuid>,
3529) -> Result<(), SqliteClientError> {
3530 let tx_ref = put_tx_data(
3531 conn,
3532 sent_tx.tx(),
3533 Some(sent_tx.fee_amount()),
3534 Some(sent_tx.created()),
3535 Some(sent_tx.target_height()),
3536 sent_tx.target_height().into(),
3537 )?;
3538
3539 let mut detectable_via_scanning = false;
3540
3541 if let Some(bundle) = sent_tx.tx().sapling_bundle() {
3550 for spend in bundle.shielded_spends() {
3551 detectable_via_scanning |=
3552 sapling::mark_sapling_note_spent(conn, tx_ref, spend.nullifier())?;
3553 }
3554 }
3555 if let Some(_bundle) = sent_tx.tx().orchard_bundle() {
3556 #[cfg(feature = "orchard")]
3557 {
3558 for action in _bundle.actions() {
3559 detectable_via_scanning |=
3560 orchard::mark_orchard_note_spent(conn, tx_ref, action.nullifier())?;
3561 }
3562 }
3563
3564 #[cfg(not(feature = "orchard"))]
3565 panic!("Sent a transaction with Orchard Actions without `orchard` enabled?");
3566 }
3567 if let Some(_bundle) = sent_tx.tx().ironwood_bundle() {
3568 #[cfg(feature = "orchard")]
3569 {
3570 for action in _bundle.actions() {
3571 detectable_via_scanning |=
3572 orchard::mark_ironwood_note_spent(conn, tx_ref, action.nullifier())?;
3573 }
3574 }
3575
3576 #[cfg(not(feature = "orchard"))]
3577 panic!("Sent a transaction with Ironwood Actions without `orchard` enabled?");
3578 }
3579
3580 #[cfg(feature = "transparent-inputs")]
3581 for utxo_outpoint in sent_tx.utxos_spent() {
3582 transparent::mark_transparent_utxo_spent(conn, tx_ref, utxo_outpoint)?;
3583 }
3584
3585 locking::unlock_spent_notes(conn, tx_ref)?;
3588
3589 for output in sent_tx.outputs() {
3590 insert_sent_output(conn, params, tx_ref, *sent_tx.funding_account(), output)?;
3591
3592 match output.recipient() {
3593 Recipient::External {
3594 recipient_address: _zaddr,
3595 output_pool: _pool,
3596 } => {
3597 #[cfg(feature = "transparent-inputs")]
3604 if _pool == &PoolType::Transparent {
3605 let address = Address::try_from_zcash_address(params, _zaddr.clone())
3606 .expect("recipient is an understood Zcash address.");
3607 if let Some(taddr) = address.to_transparent_address()
3608 && transparent::find_account_uuid_for_transparent_address(
3609 conn, params, &taddr,
3610 )?
3611 .is_some()
3612 {
3613 transparent::put_transparent_output(
3614 conn,
3615 params,
3616 gap_limits,
3617 &WalletTransparentOutput::from_parts(
3618 OutPoint::new(
3619 sent_tx.tx().txid().into(),
3620 u32::try_from(output.output_index())
3621 .expect("output index fits into a u32"),
3622 ),
3623 TxOut::new(output.value(), taddr.script().into()),
3624 None,
3625 None,
3626 Some(TransparentKeyScope::EXTERNAL),
3627 Some(*sent_tx.funding_account()),
3628 )
3629 .expect(
3630 "can extract a recipient address from an internal address script",
3631 ),
3632 sent_tx.target_height().into(),
3633 true,
3634 )?;
3635 }
3636 }
3637 }
3638 Recipient::InternalShielded {
3639 receiving_account,
3640 note,
3641 ..
3642 } => {
3643 detectable_via_scanning = true;
3646
3647 match note.as_ref() {
3648 Note::Sapling(note) => {
3649 sapling::put_received_note(
3650 conn,
3651 params,
3652 &DecryptedOutput::new(
3653 output.output_index(),
3654 note.clone(),
3655 ShieldedPool::Sapling,
3656 *receiving_account,
3657 output
3658 .memo()
3659 .map_or_else(MemoBytes::empty, |memo| memo.clone()),
3660 TransferType::AccountInternal,
3661 ),
3662 tx_ref,
3663 Some(sent_tx.target_height().into()),
3664 None,
3665 )?;
3666 }
3667 #[cfg(feature = "orchard")]
3668 orchard_note @ Note::Orchard { note, pool } => {
3669 let shielded_pool = orchard_note.pool();
3670 orchard::put_received_note(
3671 conn,
3672 params,
3673 shielded_pool,
3674 &DecryptedOutput::new(
3675 output.output_index(),
3676 (*note, *pool),
3677 shielded_pool,
3678 *receiving_account,
3679 output
3680 .memo()
3681 .map_or_else(MemoBytes::empty, |memo| memo.clone()),
3682 TransferType::AccountInternal,
3683 ),
3684 tx_ref,
3685 Some(sent_tx.target_height().into()),
3686 None,
3687 )?;
3688 }
3689 }
3690 }
3691 #[cfg(feature = "transparent-inputs")]
3692 Recipient::EphemeralTransparent {
3693 ephemeral_address,
3694 outpoint,
3695 ..
3696 } => {
3697 transparent::check_ephemeral_address_reuse(conn, params, ephemeral_address)?;
3700
3701 let (recipient_account, _) =
3703 transparent::find_account_uuid_for_transparent_address(
3704 conn,
3705 params,
3706 ephemeral_address,
3707 )?
3708 .ok_or_else(|| {
3709 SqliteClientError::CorruptedData(format!(
3710 "ephemeral address {} does not belong to any wallet account",
3711 ephemeral_address.encode(params),
3712 ))
3713 })?;
3714
3715 transparent::put_transparent_output(
3716 conn,
3717 params,
3718 gap_limits,
3719 &WalletTransparentOutput::from_parts(
3720 outpoint.clone(),
3721 TxOut::new(output.value(), ephemeral_address.script().into()),
3722 None,
3723 Some(recipient_account),
3724 Some(TransparentKeyScope::EPHEMERAL),
3725 Some(*sent_tx.funding_account()),
3726 )
3727 .expect("can extract a recipient address from an ephemeral address script"),
3728 sent_tx.target_height().into(),
3729 true,
3730 )?;
3731 }
3732 #[cfg(feature = "transparent-inputs")]
3733 Recipient::InternalTransparent {
3734 receiving_account,
3735 recipient_address,
3736 } => {
3737 transparent::put_transparent_output(
3738 conn,
3739 params,
3740 gap_limits,
3741 &WalletTransparentOutput::from_parts(
3742 OutPoint::new(
3743 sent_tx.tx().txid().into(),
3744 u32::try_from(output.output_index())
3745 .expect("output index fits into a u32"),
3746 ),
3747 TxOut::new(output.value(), recipient_address.script().into()),
3748 None,
3749 Some(*receiving_account),
3750 None,
3751 Some(*sent_tx.funding_account()),
3752 )
3753 .expect("can extract a recipient address from a transparent recipient_address"),
3754 sent_tx.target_height().into(),
3755 true,
3756 )?;
3757 }
3758 }
3759 }
3760
3761 if !detectable_via_scanning {
3766 queue_tx_status(conn, sent_tx.tx().txid())?;
3767 }
3768
3769 Ok(())
3770}
3771
3772pub(crate) fn set_transaction_status<P: consensus::Parameters>(
3773 conn: &rusqlite::Transaction,
3774 _params: &P,
3775 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
3776 txid: TxId,
3777 status: TransactionStatus,
3778) -> Result<(), SqliteClientError> {
3779 let chain_tip = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
3780
3781 match status {
3782 TransactionStatus::TxidNotRecognized | TransactionStatus::NotInMainChain => {
3783 conn.execute(
3784 "UPDATE transactions
3785 SET confirmed_unmined_at_height = :chain_tip
3786 WHERE txid = :txid
3787 AND mined_height IS NULL",
3788 named_params![
3789 ":txid": txid.as_ref(),
3790 ":chain_tip": u32::from(chain_tip)
3791 ],
3792 )?;
3793
3794 delete_retrieval_queue_entry(conn, txid, TxQueryType::Enhancement)?;
3798 conn.execute(
3799 "DELETE FROM tx_retrieval_queue
3800 WHERE txid = :txid
3801 AND query_type = :status_type
3802 AND NOT EXISTS (
3803 SELECT 1
3804 FROM transactions t
3805 WHERE t.txid = :txid
3806 AND t.mined_height IS NULL
3807 AND (
3808 t.expiry_height = 0
3809 OR (
3810 t.expiry_height > 0
3811 AND t.confirmed_unmined_at_height < t.expiry_height
3812 )
3813 OR (
3814 t.expiry_height IS NULL
3815 AND t.confirmed_unmined_at_height
3816 < t.min_observed_height + :certainty_depth
3817 )
3818 )
3819 )",
3820 named_params![
3821 ":txid": txid.as_ref(),
3822 ":status_type": TxQueryType::Status.code(),
3823 ":certainty_depth": PRUNING_DEPTH + DEFAULT_TX_EXPIRY_DELTA,
3824 ],
3825 )?;
3826 }
3827 TransactionStatus::Mined(height) => {
3828 let sql_args = named_params![
3833 ":txid": txid.as_ref(),
3834 ":height": u32::from(height)
3835 ];
3836
3837 conn.execute(
3838 "UPDATE transactions
3839 SET mined_height = :height,
3840 min_observed_height = MIN(
3841 min_observed_height,
3842 IFNULL(mined_height, :height),
3843 :height
3844 ),
3845 confirmed_unmined_at_height = NULL
3846 WHERE txid = :txid",
3847 sql_args,
3848 )?;
3849
3850 conn.execute(
3851 "UPDATE transactions
3852 SET block = blocks.height
3853 FROM blocks
3854 WHERE txid = :txid
3855 AND blocks.height = :height",
3856 sql_args,
3857 )?;
3858
3859 #[cfg(feature = "transparent-inputs")]
3860 transparent::update_gap_limits(conn, _params, gap_limits, txid, height)?;
3861
3862 delete_retrieval_queue_entry(conn, txid, TxQueryType::Enhancement)?;
3863 }
3864 }
3865
3866 Ok(())
3867}
3868
3869fn min_shared_checkpoint_height(
3873 conn: &rusqlite::Connection,
3874) -> Result<Option<BlockHeight>, SqliteClientError> {
3875 Ok(conn
3876 .query_row(
3877 "SELECT MIN(checkpoint_id) FROM (
3878 SELECT checkpoint_id FROM sapling_tree_checkpoints
3879 UNION
3880 SELECT checkpoint_id FROM orchard_tree_checkpoints
3881 UNION
3882 SELECT checkpoint_id FROM ironwood_tree_checkpoints
3883 )
3884 WHERE (checkpoint_id IN (SELECT checkpoint_id FROM sapling_tree_checkpoints)
3885 OR NOT EXISTS (SELECT 1 FROM sapling_tree_checkpoints))
3886 AND (checkpoint_id IN (SELECT checkpoint_id FROM orchard_tree_checkpoints)
3887 OR NOT EXISTS (SELECT 1 FROM orchard_tree_checkpoints))
3888 AND (checkpoint_id IN (SELECT checkpoint_id FROM ironwood_tree_checkpoints)
3889 OR NOT EXISTS (SELECT 1 FROM ironwood_tree_checkpoints))",
3890 [],
3891 |row| row.get::<_, Option<u32>>(0),
3892 )
3893 .optional()?
3894 .flatten()
3895 .map(BlockHeight::from))
3896}
3897
3898fn pool_truncation_tolerance_sql(table_prefix: &str) -> String {
3917 format!(
3918 "(height IN (SELECT checkpoint_id FROM {table_prefix}_tree_checkpoints)
3919 OR NOT EXISTS (
3920 SELECT 1 FROM {table_prefix}_tree_checkpoints WHERE checkpoint_id > height)
3921 OR (NOT EXISTS (
3922 SELECT 1 FROM {table_prefix}_tree_checkpoints WHERE checkpoint_id < height)
3923 AND NOT EXISTS (
3924 SELECT 1 FROM {table_prefix}_received_notes rn
3925 JOIN transactions tx ON tx.id_tx = rn.transaction_id
3926 WHERE tx.mined_height <= height
3927 AND rn.commitment_tree_position IS NOT NULL)))"
3928 )
3929}
3930
3931fn select_truncation_height(
3949 conn: &rusqlite::Transaction,
3950 requested_height: BlockHeight,
3951) -> Result<BlockHeight, SqliteClientError> {
3952 conn.query_row(
3953 &format!(
3954 "SELECT MAX(height) FROM blocks
3955 WHERE height <= :requested_height
3956 AND {sapling_tolerance}
3957 AND {orchard_tolerance}
3958 AND {ironwood_tolerance}",
3959 sapling_tolerance = pool_truncation_tolerance_sql(crate::SAPLING_TABLES_PREFIX),
3960 orchard_tolerance = pool_truncation_tolerance_sql(crate::ORCHARD_TABLES_PREFIX),
3961 ironwood_tolerance = pool_truncation_tolerance_sql(crate::IRONWOOD_TABLES_PREFIX),
3962 ),
3963 named_params! {":requested_height": u32::from(requested_height)},
3964 |row| row.get::<_, Option<u32>>(0),
3965 )
3966 .optional()?
3967 .flatten()
3968 .map_or_else(
3969 || {
3970 Err(SqliteClientError::RequestedRewindInvalid {
3975 safe_rewind_height: min_shared_checkpoint_height(conn)?,
3976 requested_height,
3977 })
3978 },
3979 |h| Ok(BlockHeight::from(h)),
3980 )
3981}
3982
3983pub(crate) fn truncate_to_height<P: consensus::Parameters>(
4003 conn: &rusqlite::Transaction,
4004 params: &P,
4005 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
4006 max_height: BlockHeight,
4007) -> Result<BlockHeight, SqliteClientError> {
4008 let truncation_height = select_truncation_height(conn, max_height)?;
4009 truncate_to_height_internal(
4010 conn,
4011 params,
4012 #[cfg(feature = "transparent-inputs")]
4013 gap_limits,
4014 truncation_height,
4015 truncation_height,
4016 )
4017}
4018
4019enum TreeTruncation {
4030 ToCheckpoint,
4032 Unaffected,
4038 ResetToSubtreeRoots,
4048 WouldDestroyWitnesses,
4056 DivergedCheckpoints,
4063}
4064
4065fn plan_tree_truncation(
4079 conn: &rusqlite::Transaction,
4080 table_prefix: &'static str,
4081 truncation_height: BlockHeight,
4082 rescan_floor: BlockHeight,
4083) -> Result<TreeTruncation, rusqlite::Error> {
4084 let (has_at, has_above, has_below) = conn.query_row(
4085 &format!(
4086 "SELECT
4087 EXISTS(SELECT 1 FROM {table_prefix}_tree_checkpoints
4088 WHERE checkpoint_id = :height),
4089 EXISTS(SELECT 1 FROM {table_prefix}_tree_checkpoints
4090 WHERE checkpoint_id > :height),
4091 EXISTS(SELECT 1 FROM {table_prefix}_tree_checkpoints
4092 WHERE checkpoint_id < :height)"
4093 ),
4094 named_params![":height": u32::from(truncation_height)],
4095 |row| {
4096 Ok((
4097 row.get::<_, bool>(0)?,
4098 row.get::<_, bool>(1)?,
4099 row.get::<_, bool>(2)?,
4100 ))
4101 },
4102 )?;
4103
4104 match (has_at, has_above, has_below) {
4105 (true, _, _) => Ok(TreeTruncation::ToCheckpoint),
4106 (false, false, _) => Ok(TreeTruncation::Unaffected),
4107 (false, true, false) => {
4108 let loses_witnesses = conn.query_row(
4109 &format!(
4110 "SELECT EXISTS(
4111 SELECT 1 FROM {table_prefix}_received_notes rn
4112 JOIN transactions tx ON tx.id_tx = rn.transaction_id
4113 WHERE tx.mined_height <= :height
4114 AND rn.commitment_tree_position IS NOT NULL)"
4115 ),
4116 named_params![":height": u32::from(rescan_floor)],
4117 |row| row.get::<_, bool>(0),
4118 )?;
4119 Ok(if loses_witnesses {
4120 TreeTruncation::WouldDestroyWitnesses
4121 } else {
4122 TreeTruncation::ResetToSubtreeRoots
4123 })
4124 }
4125 (false, true, true) => Ok(TreeTruncation::DivergedCheckpoints),
4126 }
4127}
4128
4129fn witness_destroying_truncation_error(
4134 conn: &rusqlite::Connection,
4135 pool: ShieldedPool,
4136 truncation_height: BlockHeight,
4137 rescan_floor: BlockHeight,
4138) -> SqliteClientError {
4139 warn!(
4140 "truncation to height {truncation_height} would discard the scanned state of the \
4141 {pool:?} note commitment tree, destroying witness data for notes received at or \
4142 below height {rescan_floor} that no rescan would re-create"
4143 );
4144 min_shared_checkpoint_height(conn).map_or_else(
4145 |e| e,
4146 |safe_rewind_height| SqliteClientError::RequestedRewindInvalid {
4147 safe_rewind_height,
4148 requested_height: rescan_floor,
4149 },
4150 )
4151}
4152
4153fn diverged_checkpoints_error(
4156 pool: ShieldedPool,
4157 truncation_height: BlockHeight,
4158) -> SqliteClientError {
4159 SqliteClientError::CorruptedData(format!(
4160 "the {pool:?} note commitment tree retains checkpoints both above and below \
4161 height {truncation_height}, but none at that height to truncate to"
4162 ))
4163}
4164
4165pub(crate) fn truncate_to_height_internal<P: consensus::Parameters>(
4179 conn: &rusqlite::Transaction,
4180 params: &P,
4181 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
4182 truncation_height: BlockHeight,
4183 rescan_floor: BlockHeight,
4184) -> Result<BlockHeight, SqliteClientError> {
4185 let last_scanned_height = conn.query_row("SELECT MAX(height) FROM blocks", [], |row| {
4186 let h = row.get::<_, Option<u32>>(0)?;
4187
4188 Ok(h.map_or_else(
4189 || {
4190 params
4191 .activation_height(NetworkUpgrade::Sapling)
4192 .map_or(BlockHeight::from_u32(0), |h| h - 1)
4194 },
4195 BlockHeight::from,
4196 ))
4197 })?;
4198
4199 trim_scan_queue_to(conn, truncation_height)?;
4204
4205 conn.execute(
4211 "UPDATE transparent_received_outputs
4212 SET max_observed_unspent_height = CASE
4213 WHEN tx.mined_height <= :height THEN :height
4214 ELSE NULL
4215 END
4216 FROM transactions tx
4217 WHERE tx.id_tx = transaction_id
4218 AND max_observed_unspent_height > :height",
4219 named_params![":height": u32::from(truncation_height)],
4220 )?;
4221
4222 conn.execute(
4225 "UPDATE transactions
4226 SET block = NULL, mined_height = NULL, tx_index = NULL, confirmed_unmined_at_height = NULL
4227 WHERE mined_height > :height",
4228 named_params![":height": u32::from(truncation_height)],
4229 )?;
4230
4231 if truncation_height < last_scanned_height {
4234 let mut wdb = WalletDb {
4237 conn: SqlTransaction(conn),
4238 params: params.clone(),
4239 clock: (),
4240 rng: (),
4241 anchor_retention_interval: AnchorRetentionInterval::default(),
4244 #[cfg(feature = "transparent-inputs")]
4245 gap_limits: *gap_limits,
4246 };
4247 match plan_tree_truncation(
4248 conn,
4249 crate::SAPLING_TABLES_PREFIX,
4250 truncation_height,
4251 rescan_floor,
4252 )? {
4253 TreeTruncation::ToCheckpoint => wdb.with_sapling_tree_mut(|tree| {
4254 let truncated =
4255 tree.truncate_to_checkpoint(&truncation_height)
4256 .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4257 pool: ShieldedPool::Sapling,
4258 height: truncation_height,
4259 error,
4260 })?;
4261 if truncated {
4262 Ok(())
4263 } else {
4264 Err(SqliteClientError::CorruptedData(format!(
4265 "the Sapling note commitment tree reported no checkpoint at height \
4266 {truncation_height} to truncate to"
4267 )))
4268 }
4269 })?,
4270 TreeTruncation::Unaffected => (),
4271 TreeTruncation::ResetToSubtreeRoots => {
4272 commitment_tree::truncate_tree_to_subtree_roots::<
4273 ::sapling::Node,
4274 { ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
4275 SAPLING_SHARD_HEIGHT,
4276 >(conn, crate::SAPLING_TABLES_PREFIX, truncation_height)
4277 .map_err(SqliteClientError::from)?
4278 }
4279 TreeTruncation::WouldDestroyWitnesses => {
4280 return Err(witness_destroying_truncation_error(
4281 conn,
4282 ShieldedPool::Sapling,
4283 truncation_height,
4284 rescan_floor,
4285 ));
4286 }
4287 TreeTruncation::DivergedCheckpoints => {
4288 return Err(diverged_checkpoints_error(
4289 ShieldedPool::Sapling,
4290 truncation_height,
4291 ));
4292 }
4293 }
4294 #[cfg(feature = "orchard")]
4295 match plan_tree_truncation(
4296 conn,
4297 crate::ORCHARD_TABLES_PREFIX,
4298 truncation_height,
4299 rescan_floor,
4300 )? {
4301 TreeTruncation::ToCheckpoint => wdb.with_orchard_tree_mut(|tree| {
4302 let truncated =
4303 tree.truncate_to_checkpoint(&truncation_height)
4304 .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4305 pool: ShieldedPool::Orchard,
4306 height: truncation_height,
4307 error,
4308 })?;
4309 if truncated {
4310 Ok(())
4311 } else {
4312 Err(SqliteClientError::CorruptedData(format!(
4313 "the Orchard note commitment tree reported no checkpoint at height \
4314 {truncation_height} to truncate to"
4315 )))
4316 }
4317 })?,
4318 TreeTruncation::Unaffected => (),
4319 TreeTruncation::ResetToSubtreeRoots => {
4320 commitment_tree::truncate_tree_to_subtree_roots::<
4321 ::orchard::tree::MerkleHashOrchard,
4322 { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
4323 ORCHARD_SHARD_HEIGHT,
4324 >(conn, crate::ORCHARD_TABLES_PREFIX, truncation_height)
4325 .map_err(SqliteClientError::from)?
4326 }
4327 TreeTruncation::WouldDestroyWitnesses => {
4328 return Err(witness_destroying_truncation_error(
4329 conn,
4330 ShieldedPool::Orchard,
4331 truncation_height,
4332 rescan_floor,
4333 ));
4334 }
4335 TreeTruncation::DivergedCheckpoints => {
4336 return Err(diverged_checkpoints_error(
4337 ShieldedPool::Orchard,
4338 truncation_height,
4339 ));
4340 }
4341 }
4342 #[cfg(feature = "orchard")]
4343 match plan_tree_truncation(
4344 conn,
4345 crate::IRONWOOD_TABLES_PREFIX,
4346 truncation_height,
4347 rescan_floor,
4348 )? {
4349 TreeTruncation::ToCheckpoint => {
4350 wdb.with_ironwood_tree_mut(|tree| {
4351 let truncated =
4352 tree.truncate_to_checkpoint(&truncation_height)
4353 .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4354 pool: ShieldedPool::Ironwood,
4355 height: truncation_height,
4356 error,
4357 })?;
4358 if truncated {
4359 Ok(())
4360 } else {
4361 Err(SqliteClientError::CorruptedData(format!(
4362 "the Ironwood note commitment tree reported no checkpoint at \
4363 height {truncation_height} to truncate to"
4364 )))
4365 }
4366 })?;
4367 }
4368 TreeTruncation::Unaffected => (),
4369 TreeTruncation::ResetToSubtreeRoots => {
4370 commitment_tree::truncate_tree_to_subtree_roots::<
4371 ::orchard::tree::MerkleHashOrchard,
4372 { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
4373 IRONWOOD_SHARD_HEIGHT,
4374 >(conn, crate::IRONWOOD_TABLES_PREFIX, truncation_height)
4375 .map_err(SqliteClientError::from)?
4376 }
4377 TreeTruncation::WouldDestroyWitnesses => {
4378 return Err(witness_destroying_truncation_error(
4379 conn,
4380 ShieldedPool::Ironwood,
4381 truncation_height,
4382 rescan_floor,
4383 ));
4384 }
4385 TreeTruncation::DivergedCheckpoints => {
4386 return Err(diverged_checkpoints_error(
4387 ShieldedPool::Ironwood,
4388 truncation_height,
4389 ));
4390 }
4391 }
4392
4393 conn.execute(
4402 "DELETE FROM blocks WHERE height > ?",
4403 [u32::from(truncation_height)],
4404 )?;
4405
4406 conn.execute(
4409 "DELETE FROM tx_locator_map
4410 WHERE block_height > :block_height",
4411 named_params![":block_height": u32::from(truncation_height)],
4412 )?;
4413 }
4414
4415 Ok(truncation_height)
4421}
4422
4423pub(crate) fn truncate_to_chain_state<P: consensus::Parameters, CL, R>(
4445 wdb: &mut WalletDb<SqlTransaction<'_>, P, CL, R>,
4446 chain_state: ChainState,
4447) -> Result<(), SqliteClientError> {
4448 let target_height = chain_state.block_height();
4449
4450 let truncate_trees = block_max_scanned(wdb.conn.0, &wdb.params)?
4456 .is_some_and(|meta| meta.block_height() > target_height);
4457
4458 if truncate_trees {
4459 match select_truncation_height(wdb.conn.0, target_height) {
4462 Ok(h) => {
4463 if h == target_height {
4464 return truncate_to_height_internal(
4467 wdb.conn.0,
4468 &wdb.params,
4469 #[cfg(feature = "transparent-inputs")]
4470 &wdb.gap_limits,
4471 h,
4472 h,
4473 )
4474 .map(|_| ());
4475 } else {
4476 }
4481 }
4482 Err(SqliteClientError::RequestedRewindInvalid {
4483 safe_rewind_height, ..
4484 }) => {
4485 if let Some(min_checkpoint_height) = safe_rewind_height {
4486 truncate_to_height_internal(
4494 wdb.conn.0,
4495 &wdb.params,
4496 #[cfg(feature = "transparent-inputs")]
4497 &wdb.gap_limits,
4498 min_checkpoint_height,
4499 min_checkpoint_height,
4500 )?;
4501 } else {
4502 }
4504 }
4505 Err(e) => {
4506 return Err(e);
4507 }
4508 };
4509
4510 wdb.with_sapling_tree_mut(|tree| {
4513 tree.insert_frontier(
4514 chain_state.final_sapling_tree().clone(),
4515 Retention::Checkpoint {
4516 id: target_height,
4517 marking: Marking::None,
4518 },
4519 )
4520 .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4521 pool: ShieldedPool::Sapling,
4522 height: target_height,
4523 error,
4524 })?;
4525 Ok::<_, SqliteClientError>(())
4526 })?;
4527
4528 #[cfg(feature = "orchard")]
4529 wdb.with_orchard_tree_mut(|tree| {
4530 tree.insert_frontier(
4531 chain_state.final_orchard_tree().clone(),
4532 Retention::Checkpoint {
4533 id: target_height,
4534 marking: Marking::None,
4535 },
4536 )
4537 .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4538 pool: ShieldedPool::Orchard,
4539 height: target_height,
4540 error,
4541 })?;
4542 Ok::<_, SqliteClientError>(())
4543 })?;
4544 #[cfg(feature = "orchard")]
4545 wdb.with_ironwood_tree_mut(|tree| {
4546 tree.insert_frontier(
4547 chain_state.final_ironwood_tree().clone(),
4548 Retention::Checkpoint {
4549 id: target_height,
4550 marking: Marking::None,
4551 },
4552 )
4553 .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4554 pool: ShieldedPool::Ironwood,
4555 height: target_height,
4556 error,
4557 })?;
4558 Ok::<_, SqliteClientError>(())
4559 })?;
4560 }
4561
4562 let truncated_height = truncate_to_height_internal(
4570 wdb.conn.0,
4571 &wdb.params,
4572 #[cfg(feature = "transparent-inputs")]
4573 &wdb.gap_limits,
4574 target_height,
4575 target_height,
4576 )?;
4577
4578 assert_eq!(truncated_height, target_height);
4579
4580 Ok(())
4581}
4582
4583pub(crate) fn rewind_to_chain_state<P: consensus::Parameters>(
4627 conn: &rusqlite::Transaction,
4628 params: &P,
4629 #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
4630 chain_state: &ChainState,
4631 reset_account_birthdays: HashSet<AccountUuid>,
4632) -> Result<(), RewindError<AccountUuid, SqliteClientError>> {
4633 let account_birthdays: HashMap<AccountUuid, BlockHeight> = {
4637 let mut stmt = conn
4638 .prepare("SELECT uuid, birthday_height FROM accounts")
4639 .map_err(|e| RewindError::DataSource(e.into()))?;
4640
4641 let rows = stmt
4642 .query_map([], |row| {
4643 let uuid: Uuid = row.get(0)?;
4644 let h: u32 = row.get(1)?;
4645 Ok((AccountUuid(uuid), BlockHeight::from(h)))
4646 })
4647 .map_err(|e| RewindError::DataSource(e.into()))?;
4648
4649 rows.collect::<Result<HashMap<_, _>, _>>()
4650 .map_err(|e| RewindError::DataSource(e.into()))?
4651 };
4652
4653 let reset_valid = reset_account_birthdays
4654 .iter()
4655 .all(|uuid| account_birthdays.contains_key(uuid));
4656
4657 if !reset_valid {
4658 return Err(RewindError::DataSource(SqliteClientError::CorruptedData(
4659 "Account UUIDs provided for birthday reset do not exist in the wallet database."
4660 .to_string(),
4661 )));
4662 }
4663
4664 let target_height = chain_state.block_height();
4665 let new_birthday = target_height + 1;
4666 let birthday_reset_required =
4673 account_birthdays.values().all(|b| b > &new_birthday) && reset_account_birthdays.is_empty();
4674
4675 if birthday_reset_required {
4676 return Err(RewindError::RewindBeyondBirthdays(account_birthdays));
4677 }
4678
4679 let chain_tip = chain_tip_height(conn).map_err(|e| RewindError::DataSource(e.into()))?;
4682
4683 if let Some(max_scanned_height) = block_max_scanned(conn, params)
4687 .map_err(RewindError::DataSource)?
4688 .map(|m| m.block_height())
4689 && target_height < max_scanned_height
4690 {
4691 let pruning_floor = max_scanned_height.saturating_sub(PRUNING_DEPTH - 1);
4693 let truncation_target = target_height.max(pruning_floor);
4694
4695 let pool_table_prefixes: &[&'static str] = &[
4705 crate::SAPLING_TABLES_PREFIX,
4706 #[cfg(feature = "orchard")]
4707 crate::ORCHARD_TABLES_PREFIX,
4708 #[cfg(feature = "orchard")]
4709 crate::IRONWOOD_TABLES_PREFIX,
4710 ];
4711 let mut window_floor: Option<BlockHeight> = None;
4712 for &table_prefix in pool_table_prefixes {
4713 let pool_floor = commitment_tree::min_checkpoint_id_at_or_above(
4714 conn,
4715 table_prefix,
4716 truncation_target,
4717 )
4718 .map_err(ShardTreeError::Storage)
4719 .map_err(SqliteClientError::from)
4720 .map_err(RewindError::DataSource)?;
4721 window_floor = window_floor.into_iter().chain(pool_floor).min();
4722 }
4723
4724 let truncation_height = window_floor.unwrap_or(pruning_floor);
4725
4726 truncate_to_height_internal(
4731 conn,
4732 params,
4733 #[cfg(feature = "transparent-inputs")]
4734 gap_limits,
4735 truncation_height,
4736 target_height,
4737 )
4738 .map_err(RewindError::DataSource)?;
4739 }
4740
4741 if let Some(t) = chain_tip
4751 && target_height < t
4752 {
4753 let rescan_range = (target_height + 1)..(t + 1);
4754 replace_queue_entries::<SqliteClientError>(
4755 conn,
4756 &rescan_range,
4757 std::iter::once(ScanRange::from_parts(
4758 rescan_range.clone(),
4759 ScanPriority::Historic,
4760 )),
4761 true,
4762 )
4763 .map_err(RewindError::DataSource)?;
4764 }
4765
4766 let new_sapling_tree_size: u64 = chain_state.final_sapling_tree().tree_size();
4767 #[cfg(feature = "orchard")]
4768 let new_orchard_tree_size = Some(chain_state.final_orchard_tree().tree_size());
4769 #[cfg(not(feature = "orchard"))]
4770 let new_orchard_tree_size: Option<u64> = None;
4771
4772 for uuid in &reset_account_birthdays {
4773 conn.execute(
4774 "UPDATE accounts
4775 SET birthday_height = :new_birthday,
4776 birthday_sapling_tree_size = :new_sapling_tree_size,
4777 birthday_orchard_tree_size = :new_orchard_tree_size
4778 WHERE uuid = :uuid AND birthday_height > :new_birthday",
4779 named_params![
4780 ":new_birthday": u32::from(new_birthday),
4781 ":new_sapling_tree_size": new_sapling_tree_size,
4782 ":new_orchard_tree_size": new_orchard_tree_size,
4783 ":uuid": uuid.0,
4784 ],
4785 )
4786 .map_err(|e| RewindError::DataSource(e.into()))?;
4787 }
4788
4789 Ok(())
4790}
4791
4792pub(crate) fn trim_scan_queue_to(
4798 conn: &rusqlite::Transaction,
4799 max_height: BlockHeight,
4800) -> Result<(), SqliteClientError> {
4801 let new_end_height = u32::from(max_height + 1);
4802 conn.execute(
4803 "DELETE FROM scan_queue
4804 WHERE block_range_start >= :new_end_height",
4805 named_params![":new_end_height": new_end_height],
4806 )?;
4807 conn.execute(
4808 "UPDATE scan_queue
4809 SET block_range_end = :new_end_height
4810 WHERE block_range_end > :new_end_height",
4811 named_params![":new_end_height": new_end_height],
4812 )?;
4813 Ok(())
4814}
4815
4816pub(crate) fn get_account_ids(
4820 conn: &rusqlite::Connection,
4821) -> Result<Vec<AccountUuid>, rusqlite::Error> {
4822 let mut stmt = conn.prepare("SELECT uuid FROM accounts")?;
4823 let mut rows = stmt.query([])?;
4824 let mut result = Vec::new();
4825 while let Some(row) = rows.next()? {
4826 let id = AccountUuid(row.get(0)?);
4827 result.push(id);
4828 }
4829 Ok(result)
4830}
4831
4832#[allow(clippy::too_many_arguments)]
4834pub(crate) fn put_block(
4835 conn: &rusqlite::Transaction<'_>,
4836 block_height: BlockHeight,
4837 block_hash: BlockHash,
4838 block_time: u32,
4839 sapling_commitment_tree_size: u32,
4840 sapling_output_count: u32,
4841 #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32,
4842 #[cfg(feature = "orchard")] orchard_action_count: u32,
4843 #[cfg(feature = "orchard")] ironwood_commitment_tree_size: u32,
4844 #[cfg(feature = "orchard")] ironwood_action_count: u32,
4845) -> Result<(), SqliteClientError> {
4846 let block_hash_data = conn
4847 .query_row(
4848 "SELECT hash FROM blocks WHERE height = ?",
4849 [u32::from(block_height)],
4850 |row| row.get::<_, Vec<u8>>(0),
4851 )
4852 .optional()?;
4853
4854 if let Some(bytes) = block_hash_data {
4857 let expected_hash = BlockHash::try_from_slice(&bytes).ok_or_else(|| {
4858 SqliteClientError::CorruptedData(format!(
4859 "Invalid block hash at height {}",
4860 u32::from(block_height)
4861 ))
4862 })?;
4863 if expected_hash != block_hash {
4864 return Err(SqliteClientError::BlockConflict(block_height));
4865 }
4866 }
4867
4868 let mut stmt_upsert_block = conn.prepare_cached(
4869 "INSERT INTO blocks (
4870 height,
4871 hash,
4872 time,
4873 sapling_commitment_tree_size,
4874 sapling_output_count,
4875 sapling_tree,
4876 orchard_commitment_tree_size,
4877 orchard_action_count,
4878 ironwood_commitment_tree_size,
4879 ironwood_action_count
4880 )
4881 VALUES (
4882 :height,
4883 :hash,
4884 :block_time,
4885 :sapling_commitment_tree_size,
4886 :sapling_output_count,
4887 x'00',
4888 :orchard_commitment_tree_size,
4889 :orchard_action_count,
4890 :ironwood_commitment_tree_size,
4891 :ironwood_action_count
4892 )
4893 ON CONFLICT (height) DO UPDATE
4894 SET hash = :hash,
4895 time = :block_time,
4896 sapling_commitment_tree_size = :sapling_commitment_tree_size,
4897 sapling_output_count = :sapling_output_count,
4898 orchard_commitment_tree_size = :orchard_commitment_tree_size,
4899 orchard_action_count = :orchard_action_count,
4900 ironwood_commitment_tree_size = :ironwood_commitment_tree_size,
4901 ironwood_action_count = :ironwood_action_count",
4902 )?;
4903
4904 #[cfg(not(feature = "orchard"))]
4905 let orchard_commitment_tree_size: Option<u32> = None;
4906 #[cfg(not(feature = "orchard"))]
4907 let orchard_action_count: Option<u32> = None;
4908 #[cfg(not(feature = "orchard"))]
4909 let ironwood_commitment_tree_size: Option<u32> = None;
4910 #[cfg(not(feature = "orchard"))]
4911 let ironwood_action_count: Option<u32> = None;
4912
4913 stmt_upsert_block.execute(named_params![
4914 ":height": u32::from(block_height),
4915 ":hash": &block_hash.0[..],
4916 ":block_time": block_time,
4917 ":sapling_commitment_tree_size": sapling_commitment_tree_size,
4918 ":sapling_output_count": sapling_output_count,
4919 ":orchard_commitment_tree_size": orchard_commitment_tree_size,
4920 ":orchard_action_count": orchard_action_count,
4921 ":ironwood_commitment_tree_size": ironwood_commitment_tree_size,
4922 ":ironwood_action_count": ironwood_action_count,
4923 ])?;
4924
4925 let mut stmt_update_transaction_block_reference = conn.prepare_cached(
4936 "UPDATE transactions
4937 SET block = :height
4938 WHERE mined_height = :height",
4939 )?;
4940
4941 stmt_update_transaction_block_reference
4942 .execute(named_params![":height": u32::from(block_height),])?;
4943
4944 Ok(())
4945}
4946
4947pub(crate) fn get_txs_spending_transparent_outputs_of<P: consensus::Parameters>(
4948 conn: &rusqlite::Connection,
4949 params: &P,
4950 tx_ref: TxRef,
4951) -> Result<Vec<(TxRef, Transaction)>, SqliteClientError> {
4952 let mut spending_txs_stmt = conn.prepare(
4955 "SELECT DISTINCT t.id_tx, t.raw, t.mined_height, t.expiry_height
4956 FROM transactions t
4957 -- find transactions that spend transparent outputs of the decrypted tx
4958 LEFT OUTER JOIN transparent_received_output_spends ts
4959 ON ts.transaction_id = t.id_tx
4960 LEFT OUTER JOIN transparent_received_outputs tro
4961 ON tro.transaction_id = :transaction_id
4962 AND tro.id = ts.transparent_received_output_id
4963 WHERE t.fee IS NULL
4964 AND t.raw IS NOT NULL
4965 AND ts.transaction_id IS NOT NULL",
4966 )?;
4967
4968 spending_txs_stmt
4969 .query_and_then(named_params![":transaction_id": tx_ref.0], |row| {
4970 let spending_tx_ref = row.get(0).map(TxRef)?;
4971 let tx_bytes: Vec<u8> = row.get(1)?;
4972 let block: Option<u32> = row.get(2)?;
4973 let expiry: Option<u32> = row.get(3)?;
4974
4975 let (_, spending_tx) = parse_tx(
4976 params,
4977 &tx_bytes,
4978 block.map(BlockHeight::from),
4979 expiry.map(BlockHeight::from),
4980 )?;
4981
4982 Ok((spending_tx_ref, spending_tx))
4983 })?
4984 .collect()
4985}
4986
4987pub(crate) fn update_tx_fee(
4988 conn: &rusqlite::Transaction<'_>,
4989 tx_ref: TxRef,
4990 fee: zcash_protocol::value::Zatoshis,
4991) -> Result<(), SqliteClientError> {
4992 conn.execute(
4993 "UPDATE transactions
4994 SET fee = :fee
4995 WHERE id_tx = :transaction_id",
4996 named_params! {
4997 ":transaction_id": tx_ref.0,
4998 ":fee": u64::from(fee)
4999 },
5000 )?;
5001
5002 Ok(())
5003}
5004
5005pub(crate) fn set_tx_trust(
5006 conn: &rusqlite::Transaction,
5007 txid: TxId,
5008 trusted: bool,
5009) -> Result<(), SqliteClientError> {
5010 conn.execute(
5011 "UPDATE transactions
5012 SET trust_status = :trust_status
5013 WHERE txid = :txid",
5014 named_params! {
5015 ":txid": &txid.as_ref()[..],
5016 ":trust_status": trusted
5017 },
5018 )?;
5019
5020 Ok(())
5021}
5022
5023pub(crate) fn put_tx_meta(
5026 conn: &rusqlite::Connection,
5027 tx: &WalletTx<AccountUuid>,
5028 height: BlockHeight,
5029) -> Result<TxRef, SqliteClientError> {
5030 let mut stmt_upsert_tx_meta = conn.prepare_cached(
5032 "INSERT INTO transactions (txid, block, mined_height, tx_index, min_observed_height)
5033 VALUES (:txid, :block, :block, :tx_index, :block)
5034 ON CONFLICT (txid) DO UPDATE
5035 SET block = :block,
5036 mined_height = :block,
5037 tx_index = :tx_index,
5038 min_observed_height = MIN(min_observed_height, :block),
5039 confirmed_unmined_at_height = NULL
5040 RETURNING id_tx",
5041 )?;
5042
5043 let txid_bytes = tx.txid();
5044 let tx_params = named_params![
5045 ":txid": &txid_bytes.as_ref()[..],
5046 ":block": u32::from(height),
5047 ":tx_index": u16::from(tx.block_index()),
5048 ];
5049
5050 stmt_upsert_tx_meta
5051 .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef))
5052 .map_err(SqliteClientError::from)
5053}
5054
5055pub(crate) fn select_receiving_address<P: consensus::Parameters>(
5058 conn: &rusqlite::Connection,
5059 _params: &P,
5060 account: AccountUuid,
5061 receiver: &Receiver,
5062) -> Result<Option<ZcashAddress>, SqliteClientError> {
5063 match receiver {
5064 #[cfg(feature = "transparent-inputs")]
5065 Receiver::Transparent(taddr) => conn
5066 .query_row(
5067 "SELECT address
5068 FROM addresses
5069 WHERE cached_transparent_receiver_address = :taddr",
5070 named_params! {
5071 ":taddr": Address::Transparent(*taddr).encode(_params)
5072 },
5073 |row| row.get::<_, String>(0),
5074 )
5075 .optional()?
5076 .map(|addr_str| addr_str.parse::<ZcashAddress>())
5077 .transpose()
5078 .map_err(SqliteClientError::from),
5079 receiver => {
5080 let mut stmt = conn.prepare_cached(
5081 "SELECT address
5082 FROM addresses
5083 JOIN accounts ON accounts.id = addresses.account_id
5084 WHERE accounts.uuid = :account_uuid
5085 AND key_scope = :key_scope",
5086 )?;
5087
5088 let mut result = stmt.query(named_params! {
5089 ":account_uuid": account.0,
5090 ":key_scope": KeyScope::EXTERNAL.encode(),
5091 })?;
5092 while let Some(row) = result.next()? {
5093 let addr_str = row.get::<_, String>(0)?;
5094 let decoded = addr_str.parse::<ZcashAddress>()?;
5095 if receiver.corresponds(&decoded) {
5096 return Ok(Some(decoded));
5097 }
5098 }
5099
5100 Ok(None)
5101 }
5102 }
5103}
5104
5105pub(crate) fn put_tx_data(
5107 conn: &rusqlite::Connection,
5108 tx: &Transaction,
5109 fee: Option<Zatoshis>,
5110 created_at: Option<time::OffsetDateTime>,
5111 target_height: Option<TargetHeight>,
5112 observed_height: BlockHeight,
5113) -> Result<TxRef, SqliteClientError> {
5114 let mut stmt_upsert_tx_data = conn.prepare_cached(
5115 "INSERT INTO transactions (txid, tx_index, created, expiry_height, raw, fee, target_height, min_observed_height)
5116 VALUES (:txid, :tx_index, :created_at, :expiry_height, :raw, :fee, :target_height, :observed_height)
5117 ON CONFLICT (txid) DO UPDATE
5118 SET expiry_height = :expiry_height,
5119 raw = :raw,
5120 fee = IFNULL(:fee, fee),
5121 tx_index = IFNULL(tx_index, :tx_index),
5122 min_observed_height = MIN(
5123 min_observed_height,
5124 :observed_height
5125 )
5126 RETURNING id_tx",
5127 )?;
5128
5129 let txid = tx.txid();
5130 let mut raw_tx = vec![];
5131 tx.write(&mut raw_tx)?;
5132
5133 let tx_index = tx
5134 .transparent_bundle()
5135 .and_then(|bundle| bundle.is_coinbase().then_some(0i64));
5136
5137 let tx_params = named_params![
5138 ":txid": &txid.as_ref()[..],
5139 ":tx_index": tx_index,
5140 ":created_at": created_at,
5141 ":expiry_height": u32::from(tx.expiry_height()),
5142 ":raw": raw_tx,
5143 ":fee": fee.map(u64::from),
5144 ":target_height": target_height.map(u32::from),
5145 ":observed_height": u32::from(observed_height)
5146 ];
5147
5148 stmt_upsert_tx_data
5149 .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef))
5150 .map_err(SqliteClientError::from)
5151}
5152
5153pub(crate) fn put_zip318_classification(
5160 conn: &rusqlite::Connection,
5161 tx_ref: TxRef,
5162 classification: zcash_protocol::zip318::Zip318Classification,
5163) -> Result<(), SqliteClientError> {
5164 conn.execute(
5165 "UPDATE transactions SET zip318_kind = :zip318_kind WHERE id_tx = :id_tx",
5166 named_params![
5167 ":zip318_kind": classification.to_code(),
5168 ":id_tx": tx_ref.0,
5169 ],
5170 )?;
5171
5172 Ok(())
5173}
5174
5175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5176pub(crate) enum TxQueryType {
5177 Status,
5178 Enhancement,
5179}
5180
5181impl TxQueryType {
5182 pub(crate) fn code(&self) -> i64 {
5183 match self {
5184 TxQueryType::Status => 0,
5185 TxQueryType::Enhancement => 1,
5186 }
5187 }
5188
5189 pub(crate) fn from_code(code: i64) -> Option<Self> {
5190 match code {
5191 0 => Some(TxQueryType::Status),
5192 1 => Some(TxQueryType::Enhancement),
5193 _ => None,
5194 }
5195 }
5196}
5197
5198#[cfg(feature = "transparent-inputs")]
5199pub(crate) fn queue_transparent_input_retrieval<AccountId>(
5200 conn: &rusqlite::Transaction<'_>,
5201 tx_ref: TxRef,
5202 d_tx: &DecryptedTransaction<Transaction, AccountId>,
5203) -> Result<(), SqliteClientError> {
5204 if let Some(b) = d_tx.tx().transparent_bundle()
5205 && !b.is_coinbase()
5206 {
5207 queue_tx_retrieval(
5209 conn,
5210 b.vin.iter().map(|txin| *txin.prevout().txid()),
5211 Some(tx_ref),
5212 )?;
5213 }
5214
5215 Ok(())
5216}
5217
5218pub(crate) fn queue_tx_retrieval(
5219 conn: &rusqlite::Transaction<'_>,
5220 txids: impl Iterator<Item = TxId>,
5221 dependent_tx_ref: Option<TxRef>,
5222) -> Result<(), SqliteClientError> {
5223 let mut stmt_insert_tx = conn.prepare_cached(
5227 "INSERT INTO tx_retrieval_queue (txid, query_type, dependent_transaction_id)
5228 SELECT
5229 :txid,
5230 :enhancement_type,
5231 :dependent_transaction_id
5232 WHERE NOT EXISTS (
5233 SELECT 1 FROM transactions WHERE txid = :txid AND raw IS NOT NULL
5234 )
5235 ON CONFLICT (txid, query_type) DO UPDATE
5236 SET dependent_transaction_id =
5237 IFNULL(:dependent_transaction_id, dependent_transaction_id)",
5238 )?;
5239
5240 for txid in txids {
5241 stmt_insert_tx.execute(named_params! {
5242 ":txid": txid.as_ref(),
5243 ":enhancement_type": TxQueryType::Enhancement.code(),
5244 ":dependent_transaction_id": dependent_tx_ref.map(|r| r.0),
5245 })?;
5246 }
5247
5248 Ok(())
5249}
5250
5251pub(crate) fn queue_tx_status(
5255 conn: &rusqlite::Transaction<'_>,
5256 txid: TxId,
5257) -> Result<(), SqliteClientError> {
5258 conn.execute(
5259 "INSERT INTO tx_retrieval_queue (txid, query_type)
5260 VALUES (:txid, :status_type)
5261 ON CONFLICT (txid, query_type) DO NOTHING",
5262 named_params![
5263 ":txid": txid.as_ref(),
5264 ":status_type": TxQueryType::Status.code(),
5265 ],
5266 )?;
5267
5268 Ok(())
5269}
5270
5271pub(crate) fn transaction_data_requests(
5274 conn: &rusqlite::Connection,
5275) -> Result<Vec<TransactionDataRequest>, SqliteClientError> {
5276 let mut tx_retrieval_stmt = conn.prepare_cached(
5277 "SELECT q.txid, q.query_type
5278 FROM tx_retrieval_queue q
5279 LEFT JOIN transactions t ON t.txid = q.txid
5280 WHERE q.query_type = :enhancement_type
5281 OR (
5282 q.query_type = :status_type
5283 AND t.mined_height IS NULL
5284 AND (
5285 t.confirmed_unmined_at_height IS NULL
5286 OR t.expiry_height = 0
5287 OR (
5288 t.expiry_height > 0
5289 AND t.confirmed_unmined_at_height < t.expiry_height
5290 )
5291 OR (
5292 t.expiry_height IS NULL
5293 AND t.confirmed_unmined_at_height
5294 < t.min_observed_height + :certainty_depth
5295 )
5296 )
5297 )",
5298 )?;
5299
5300 let result = tx_retrieval_stmt
5301 .query_and_then(
5302 named_params![
5303 ":status_type": TxQueryType::Status.code(),
5304 ":enhancement_type": TxQueryType::Enhancement.code(),
5305 ":certainty_depth": PRUNING_DEPTH + DEFAULT_TX_EXPIRY_DELTA
5306 ],
5307 |row| {
5308 let txid = row.get(0).map(TxId::from_bytes)?;
5309 let query_type = row.get(1).map(TxQueryType::from_code)?.ok_or_else(|| {
5310 SqliteClientError::CorruptedData(
5311 "Unrecognized transaction data request type.".to_owned(),
5312 )
5313 })?;
5314
5315 Ok::<TransactionDataRequest, SqliteClientError>(match query_type {
5316 TxQueryType::Status => TransactionDataRequest::GetStatus(txid),
5317 TxQueryType::Enhancement => TransactionDataRequest::Enhancement(txid),
5318 })
5319 },
5320 )?
5321 .collect::<Result<Vec<_>, _>>()?;
5322
5323 Ok(result)
5324}
5325
5326pub(crate) fn delete_retrieval_queue_entries(
5327 conn: &rusqlite::Transaction<'_>,
5328 txid: TxId,
5329) -> Result<(), SqliteClientError> {
5330 delete_retrieval_queue_entry(conn, txid, TxQueryType::Enhancement)
5331}
5332
5333fn delete_retrieval_queue_entry(
5334 conn: &rusqlite::Transaction<'_>,
5335 txid: TxId,
5336 query_type: TxQueryType,
5337) -> Result<(), SqliteClientError> {
5338 conn.execute(
5339 "DELETE FROM tx_retrieval_queue
5340 WHERE txid = :txid
5341 AND query_type = :query_type",
5342 named_params![
5343 ":txid": txid.as_ref(),
5344 ":query_type": query_type.code(),
5345 ],
5346 )?;
5347
5348 Ok(())
5349}
5350
5351fn recipient_params<P: consensus::Parameters>(
5354 conn: &Connection,
5355 _params: &P,
5356 from: AccountUuid,
5357 to: &Recipient<AccountUuid>,
5358) -> Result<(AccountRef, Option<String>, Option<AccountRef>, PoolType), SqliteClientError> {
5359 let from_account_id = get_account_ref(conn, from)?;
5360 match to {
5361 Recipient::External {
5362 recipient_address,
5363 output_pool,
5364 ..
5365 } => Ok((
5366 from_account_id,
5367 Some(recipient_address.encode()),
5368 None,
5369 *output_pool,
5370 )),
5371 #[cfg(feature = "transparent-inputs")]
5372 Recipient::EphemeralTransparent {
5373 receiving_account,
5374 ephemeral_address,
5375 ..
5376 } => {
5377 let to_account = get_account_ref(conn, *receiving_account)?;
5378 Ok((
5379 from_account_id,
5380 Some(ephemeral_address.encode(_params)),
5381 Some(to_account),
5382 PoolType::TRANSPARENT,
5383 ))
5384 }
5385 #[cfg(feature = "transparent-inputs")]
5386 Recipient::InternalTransparent {
5387 receiving_account,
5388 recipient_address,
5389 } => {
5390 let to_account = get_account_ref(conn, *receiving_account)?;
5391 Ok((
5392 from_account_id,
5393 Some(recipient_address.encode(_params)),
5394 Some(to_account),
5395 PoolType::TRANSPARENT,
5396 ))
5397 }
5398 Recipient::InternalShielded {
5399 receiving_account,
5400 external_address,
5401 note,
5402 } => {
5403 let to_account = get_account_ref(conn, *receiving_account)?;
5404 Ok((
5405 from_account_id,
5406 external_address.as_ref().map(|a| a.encode()),
5407 Some(to_account),
5408 PoolType::Shielded(note.pool()),
5409 ))
5410 }
5411 }
5412}
5413
5414fn flag_previously_received_change(
5415 conn: &rusqlite::Transaction,
5416 tx_ref: TxRef,
5417) -> Result<(), SqliteClientError> {
5418 let flag_received_change = |protocol| {
5419 let TableConstants { table_prefix, .. } = table_constants::<SqliteClientError>(protocol)?;
5420 conn.execute(
5421 &format!(
5422 "UPDATE {table_prefix}_received_notes
5423 SET is_change = 1
5424 FROM sent_notes sn
5425 WHERE sn.transaction_id = {table_prefix}_received_notes.transaction_id
5426 AND sn.transaction_id = :transaction_id
5427 AND sn.from_account_id = {table_prefix}_received_notes.account_id
5428 AND {table_prefix}_received_notes.recipient_key_scope = :internal_scope"
5429 ),
5430 named_params! {
5431 ":transaction_id": tx_ref.0,
5432 ":internal_scope": KeyScope::INTERNAL.encode()
5433 },
5434 )
5435 .map_err(SqliteClientError::from)
5436 };
5437
5438 flag_received_change(ShieldedPool::Sapling)?;
5444 #[cfg(feature = "orchard")]
5445 flag_received_change(ShieldedPool::Orchard)?;
5446 #[cfg(feature = "orchard")]
5447 flag_received_change(ShieldedPool::Ironwood)?;
5448
5449 Ok(())
5450}
5451
5452pub(crate) fn insert_sent_output<P: consensus::Parameters>(
5454 conn: &rusqlite::Transaction,
5455 params: &P,
5456 tx_ref: TxRef,
5457 from_account_uuid: AccountUuid,
5458 output: &SentTransactionOutput<AccountUuid>,
5459) -> Result<(), SqliteClientError> {
5460 let mut stmt_insert_sent_output = conn.prepare_cached(
5461 "INSERT INTO sent_notes (
5462 transaction_id, output_pool, output_index, from_account_id,
5463 to_address, to_account_id, value, memo)
5464 VALUES (
5465 :transaction_id, :output_pool, :output_index, :from_account_id,
5466 :to_address, :to_account_id, :value, :memo)",
5467 )?;
5468
5469 let (from_account_id, to_address, to_account_id, pool_type) =
5470 recipient_params(conn, params, from_account_uuid, output.recipient())?;
5471 let sql_args = named_params![
5472 ":transaction_id": tx_ref.0,
5473 ":output_pool": &pool_code(pool_type),
5474 ":output_index": &i64::try_from(output.output_index()).unwrap(),
5475 ":from_account_id": from_account_id.0,
5476 ":to_address": &to_address,
5477 ":to_account_id": to_account_id.map(|a| a.0),
5478 ":value": &i64::from(ZatBalance::from(output.value())),
5479 ":memo": memo_repr(output.memo())
5480 ];
5481
5482 stmt_insert_sent_output.execute(sql_args)?;
5483 flag_previously_received_change(conn, tx_ref)?;
5484
5485 Ok(())
5486}
5487
5488#[allow(clippy::too_many_arguments)]
5500pub(crate) fn put_sent_output<P: consensus::Parameters>(
5501 conn: &rusqlite::Transaction,
5502 params: &P,
5503 from_account_uuid: AccountUuid,
5504 tx_ref: TxRef,
5505 output_index: usize,
5506 recipient: &Recipient<AccountUuid>,
5507 value: Zatoshis,
5508 memo: Option<&MemoBytes>,
5509) -> Result<(), SqliteClientError> {
5510 let mut stmt_upsert_sent_output = conn.prepare_cached(
5511 "INSERT INTO sent_notes (
5512 transaction_id, output_pool, output_index, from_account_id,
5513 to_address, to_account_id, value, memo)
5514 VALUES (
5515 :transaction_id, :output_pool, :output_index, :from_account_id,
5516 :to_address, :to_account_id, :value, :memo)
5517 ON CONFLICT (transaction_id, output_pool, output_index) DO UPDATE
5518 SET from_account_id = :from_account_id,
5519 to_address = IFNULL(to_address, :to_address),
5520 to_account_id = IFNULL(to_account_id, :to_account_id),
5521 value = :value,
5522 memo = IFNULL(:memo, memo)",
5523 )?;
5524
5525 let (from_account_id, to_address, to_account_id, pool_type) =
5526 recipient_params(conn, params, from_account_uuid, recipient)?;
5527 let sql_args = named_params![
5528 ":transaction_id": tx_ref.0,
5529 ":output_pool": &pool_code(pool_type),
5530 ":output_index": &i64::try_from(output_index).unwrap(),
5531 ":from_account_id": from_account_id.0,
5532 ":to_address": &to_address,
5533 ":to_account_id": &to_account_id.map(|a| a.0),
5534 ":value": &i64::from(ZatBalance::from(value)),
5535 ":memo": memo_repr(memo)
5536 ];
5537
5538 stmt_upsert_sent_output.execute(sql_args)?;
5539 flag_previously_received_change(conn, tx_ref)?;
5540
5541 Ok(())
5542}
5543
5544pub(crate) fn insert_nullifier_map<N: AsRef<[u8]>>(
5550 conn: &rusqlite::Transaction<'_>,
5551 block_height: BlockHeight,
5552 spend_pool: ShieldedPool,
5553 new_entries: &[(TxIndex, TxId, Vec<N>)],
5554) -> Result<(), SqliteClientError> {
5555 let mut stmt_select_tx_locators = conn.prepare_cached(
5556 "SELECT block_height, tx_index, txid
5557 FROM tx_locator_map
5558 WHERE (block_height = :block_height AND tx_index = :tx_index) OR txid = :txid",
5559 )?;
5560 let mut stmt_insert_tx_locator = conn.prepare_cached(
5561 "INSERT INTO tx_locator_map
5562 (block_height, tx_index, txid)
5563 VALUES (:block_height, :tx_index, :txid)",
5564 )?;
5565 let mut stmt_insert_nullifier_mapping = conn.prepare_cached(
5566 "INSERT INTO nullifier_map
5567 (spend_pool, nf, block_height, tx_index)
5568 VALUES (:spend_pool, :nf, :block_height, :tx_index)
5569 ON CONFLICT (spend_pool, nf) DO UPDATE
5570 SET block_height = :block_height,
5571 tx_index = :tx_index",
5572 )?;
5573
5574 for (tx_index, txid, nullifiers) in new_entries {
5575 let tx_args = named_params![
5576 ":block_height": u32::from(block_height),
5577 ":tx_index": u16::from(*tx_index),
5578 ":txid": txid.as_ref(),
5579 ];
5580
5581 let locator = stmt_select_tx_locators
5592 .query_map(tx_args, |row| {
5593 Ok((
5594 BlockHeight::from_u32(row.get(0)?),
5595 TxIndex::from(row.get::<_, u16>(1)?),
5596 TxId::from_bytes(row.get(2)?),
5597 ))
5598 })?
5599 .try_fold(None, |acc, row| -> Result<_, SqliteClientError> {
5600 match (acc, row?) {
5601 (None, rhs) => Ok(Some(Some(rhs))),
5602 (Some(_), _) => Ok(Some(None)),
5606 }
5607 })?;
5608
5609 match locator {
5610 Some(Some(loc)) if loc == (block_height, *tx_index, *txid) => (),
5612 Some(_) => Err(SqliteClientError::DbError(rusqlite::Error::SqliteFailure(
5614 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT),
5615 Some("UNIQUE constraint failed: tx_locator_map.block_height, tx_locator_map.tx_index".into()),
5616 )))?,
5617 None => stmt_insert_tx_locator.execute(tx_args).map(|_| ())?,
5619 }
5620
5621 for nf in nullifiers {
5622 let nf_args = named_params![
5625 ":spend_pool": pool_code(PoolType::Shielded(spend_pool)),
5626 ":nf": nf.as_ref(),
5627 ":block_height": u32::from(block_height),
5628 ":tx_index": u16::from(*tx_index),
5629 ];
5630 stmt_insert_nullifier_mapping.execute(nf_args)?;
5631 }
5632 }
5633
5634 Ok(())
5635}
5636
5637pub(crate) fn query_nullifier_map<N: AsRef<[u8]>>(
5640 conn: &rusqlite::Transaction<'_>,
5641 spend_pool: ShieldedPool,
5642 nf: &N,
5643) -> Result<Option<TxRef>, SqliteClientError> {
5644 let mut stmt_select_locator = conn.prepare_cached(
5645 "SELECT block_height, tx_index, txid
5646 FROM nullifier_map
5647 LEFT JOIN tx_locator_map USING (block_height, tx_index)
5648 WHERE spend_pool = :spend_pool AND nf = :nf",
5649 )?;
5650
5651 let sql_args = named_params![
5652 ":spend_pool": pool_code(PoolType::Shielded(spend_pool)),
5653 ":nf": nf.as_ref(),
5654 ];
5655
5656 let locator = stmt_select_locator
5658 .query_row(sql_args, |row| {
5659 Ok((
5660 BlockHeight::from_u32(row.get(0)?),
5661 TxIndex::from(row.get::<_, u16>(1)?),
5662 TxId::from_bytes(row.get(2)?),
5663 ))
5664 })
5665 .optional()?;
5666 let (height, index, txid) = match locator {
5667 Some(res) => res,
5668 None => return Ok(None),
5669 };
5670
5671 put_tx_meta(
5676 conn,
5677 &WalletTx::new(
5678 txid,
5679 index,
5680 vec![],
5681 vec![],
5682 vec![],
5683 #[cfg(feature = "orchard")]
5684 vec![],
5685 #[cfg(feature = "orchard")]
5686 vec![],
5687 #[cfg(feature = "orchard")]
5688 vec![],
5689 #[cfg(feature = "orchard")]
5690 vec![],
5691 ),
5692 height,
5693 )
5694 .map(Some)
5695}
5696
5697pub(crate) fn prune_nullifier_map(
5700 conn: &rusqlite::Transaction<'_>,
5701 block_height: BlockHeight,
5702) -> Result<(), SqliteClientError> {
5703 let mut stmt_delete_locators = conn.prepare_cached(
5704 "DELETE FROM tx_locator_map
5705 WHERE block_height < :block_height",
5706 )?;
5707
5708 stmt_delete_locators.execute(named_params![":block_height": u32::from(block_height)])?;
5709
5710 Ok(())
5711}
5712
5713pub(crate) fn get_block_range(
5714 conn: &rusqlite::Connection,
5715 protocol: ShieldedPool,
5716 commitment_tree_address: incrementalmerkletree::Address,
5717) -> Result<Option<Range<BlockHeight>>, SqliteClientError> {
5718 let prefix = match protocol {
5719 ShieldedPool::Sapling => "sapling",
5720 ShieldedPool::Orchard => "orchard",
5721 ShieldedPool::Ironwood => "ironwood",
5722 };
5723 let mut stmt = conn.prepare_cached(&format!(
5724 "SELECT MIN(height), MAX(height), MAX({prefix}_commitment_tree_size)
5725 FROM blocks
5726 WHERE {prefix}_commitment_tree_size BETWEEN :min_tree_size AND :max_tree_size"
5727 ))?;
5728
5729 stmt.query_row(
5730 named_params! {
5734 ":min_tree_size": u64::from(commitment_tree_address.position_range_start()) + 1,
5735 ":max_tree_size": u64::from(commitment_tree_address.position_range_end()),
5736 },
5737 |row| {
5738 let min_height = row.get::<_, Option<u32>>(0)?.map(BlockHeight::from_u32);
5742 let max_height_inclusive = row.get::<_, Option<u32>>(1)?.map(BlockHeight::from_u32);
5743 let end_offset = row.get::<_, Option<u64>>(2)?.map(|max_height_tree_size| {
5744 if max_height_tree_size < u64::from(commitment_tree_address.position_range_end()) {
5750 1
5751 } else {
5752 0
5753 }
5754 });
5755
5756 Ok(min_height
5757 .zip(max_height_inclusive)
5758 .zip(end_offset)
5759 .map(|((min, max_inclusive), offset)| min..(max_inclusive + offset + 1)))
5760 },
5761 )
5762 .map_err(SqliteClientError::from)
5763}
5764
5765pub(crate) fn get_received_outputs(
5766 conn: &rusqlite::Connection,
5767 txid: TxId,
5768 target_height: TargetHeight,
5769 confirmations_policy: ConfirmationsPolicy,
5770) -> Result<Vec<ReceivedTransactionOutput>, SqliteClientError> {
5771 let mut stmt_received_outputs = conn.prepare_cached(
5772 "SELECT
5773 vto.output_pool,
5774 vto.output_index,
5775 vto.recipient_key_scope,
5776 vto.value,
5777 vto.tx_mined_height,
5778 IFNULL(vto.tx_trust_status, 0) AS tx_trust_status,
5779 MAX(tt.mined_height) AS max_shielding_input_height,
5780 MIN(IFNULL(tt.trust_status, 0)) AS min_shielding_input_trust
5781 FROM v_tx_outputs vto
5782 LEFT OUTER JOIN transparent_received_output_spends ros
5783 ON ros.transaction_id = vto.transaction_id
5784 LEFT OUTER JOIN transparent_received_outputs tro
5785 ON tro.id = ros.transparent_received_output_id
5786 LEFT OUTER JOIN transactions tt
5787 ON tt.id_tx = tro.transaction_id
5788 WHERE vto.txid = :txid
5789 GROUP BY vto.output_pool, vto.output_index",
5790 )?;
5791
5792 let results = stmt_received_outputs
5793 .query_and_then::<_, SqliteClientError, _, _>(
5794 named_params![":txid": txid.as_ref()],
5795 |row| {
5796 let pool_type = parse_pool_code(row.get("output_pool")?)?;
5797 let output_index = row.get("output_index")?;
5798 let value = Zatoshis::from_nonnegative_i64(row.get("value")?)?;
5799 let mined_height = row
5800 .get::<_, Option<u32>>("tx_mined_height")?
5801 .map(BlockHeight::from);
5802 let max_shielding_input_height = row
5803 .get::<_, Option<u32>>("max_shielding_input_height")?
5804 .map(BlockHeight::from);
5805 let tx_shielding_inputs_trusted =
5806 row.get::<_, bool>("min_shielding_input_trust")?;
5807 let key_scope = row
5808 .get::<_, Option<i64>>("recipient_key_scope")?
5809 .map(KeyScope::decode)
5810 .transpose()?;
5811 let tx_trusted = row.get::<_, bool>("tx_trust_status")?;
5812
5813 let confirmations_until_spendable = confirmations_policy
5814 .confirmations_until_spendable(
5815 target_height,
5816 pool_type,
5817 key_scope.and_then(|s| zip32::Scope::try_from(s).ok()),
5818 mined_height,
5819 tx_trusted,
5820 max_shielding_input_height,
5821 tx_shielding_inputs_trusted,
5822 );
5823
5824 Ok(ReceivedTransactionOutput::from_parts(
5825 pool_type,
5826 output_index,
5827 value,
5828 confirmations_until_spendable,
5829 ))
5830 },
5831 )?
5832 .collect::<Result<Vec<_>, _>>()?;
5833
5834 Ok(results)
5835}
5836
5837#[cfg(any(test, feature = "test-dependencies"))]
5839pub mod testing {
5840 use incrementalmerkletree::Position;
5841 use zcash_client_backend::data_api::testing::TransactionSummary;
5842 use zcash_primitives::transaction::TxId;
5843 use zcash_protocol::{
5844 ShieldedPool,
5845 consensus::BlockHeight,
5846 value::{ZatBalance, Zatoshis},
5847 };
5848
5849 use super::common::{TableConstants, table_constants};
5850 use crate::{AccountUuid, error::SqliteClientError};
5851
5852 pub(crate) fn get_tx_history(
5853 conn: &rusqlite::Connection,
5854 ) -> Result<Vec<TransactionSummary<AccountUuid>>, SqliteClientError> {
5855 let mut stmt = conn.prepare_cached(
5856 "SELECT accounts.uuid as account_uuid, v_transactions.*
5857 FROM v_transactions
5858 JOIN accounts ON accounts.uuid = v_transactions.account_uuid
5859 ORDER BY mined_height DESC, tx_index DESC",
5860 )?;
5861
5862 let results = stmt
5863 .query_and_then::<_, SqliteClientError, _, _>([], |row| {
5864 Ok(TransactionSummary::from_parts(
5865 AccountUuid(row.get("account_uuid")?),
5866 TxId::from_bytes(row.get("txid")?),
5867 row.get::<_, Option<u32>>("expiry_height")?
5868 .map(BlockHeight::from),
5869 row.get::<_, Option<u32>>("mined_height")?
5870 .map(BlockHeight::from),
5871 ZatBalance::from_i64(row.get("account_balance_delta")?)?,
5872 Zatoshis::from_nonnegative_i64(row.get("total_spent")?)?,
5873 Zatoshis::from_nonnegative_i64(row.get("total_received")?)?,
5874 row.get::<_, Option<i64>>("fee_paid")?
5875 .map(Zatoshis::from_nonnegative_i64)
5876 .transpose()?,
5877 row.get("spent_note_count")?,
5878 row.get("has_change")?,
5879 row.get("sent_note_count")?,
5880 row.get("received_note_count")?,
5881 row.get("memo_count")?,
5882 row.get("expired_unmined")?,
5883 row.get("is_shielding")?,
5884 row.get::<_, Option<i64>>("pool_crossing_value")?
5885 .map(Zatoshis::from_nonnegative_i64)
5886 .transpose()?,
5887 ))
5888 })?
5889 .collect::<Result<Vec<_>, _>>()?;
5890
5891 Ok(results)
5892 }
5893
5894 #[allow(dead_code)] pub(crate) fn get_checkpoint_history(
5897 conn: &rusqlite::Connection,
5898 protocol: ShieldedPool,
5899 ) -> Result<Vec<(BlockHeight, Option<Position>)>, SqliteClientError> {
5900 let TableConstants { table_prefix, .. } = table_constants::<SqliteClientError>(protocol)?;
5901
5902 let mut stmt = conn.prepare_cached(&format!(
5903 "SELECT checkpoint_id, position FROM {table_prefix}_tree_checkpoints
5904 ORDER BY checkpoint_id",
5905 ))?;
5906
5907 let results = stmt
5908 .query_and_then::<_, SqliteClientError, _, _>([], |row| {
5909 Ok((
5910 BlockHeight::from(row.get::<_, u32>(0)?),
5911 row.get::<_, Option<u64>>(1)?.map(Position::from),
5912 ))
5913 })?
5914 .collect::<Result<Vec<_>, _>>()?;
5915
5916 Ok(results)
5917 }
5918}
5919
5920#[cfg(test)]
5921mod tests {
5922 use std::{
5923 collections::HashSet,
5924 num::{NonZeroU8, NonZeroU32},
5925 };
5926
5927 use rusqlite::{Connection, named_params};
5928 use sapling::zip32::ExtendedSpendingKey;
5929 use secrecy::{ExposeSecret, SecretVec};
5930 use uuid::Uuid;
5931 use zcash_client_backend::data_api::{
5932 Account as _, AccountSource, TransactionDataRequest, TransactionStatus, WalletRead,
5933 WalletWrite,
5934 chain::{ChainState, CommitmentTreeRoot},
5935 error::RewindError,
5936 testing::{
5937 AddressType, DataStoreFactory, FakeCompactOutput, InitialChainState, TestBuilder,
5938 TestState, pool::ShieldedPoolTester, sapling::SaplingPoolTester,
5939 },
5940 wallet::ConfirmationsPolicy,
5941 };
5942 use zcash_keys::keys::UnifiedAddressRequest;
5943 use zcash_primitives::block::BlockHash;
5944 use zcash_protocol::{
5945 TxId,
5946 consensus::{BlockHeight, NetworkUpgrade, Parameters},
5947 value::Zatoshis,
5948 };
5949
5950 use crate::{
5951 AccountUuid,
5952 error::SqliteClientError,
5953 testing::{BlockCache, db::TestDbFactory},
5954 };
5955
5956 use super::{
5957 KeyScope, ShieldedPool, TxQueryType, TxRef, account_birthday,
5958 flag_previously_received_change, min_shared_checkpoint_height, queue_tx_retrieval,
5959 select_truncation_height,
5960 };
5961
5962 use incrementalmerkletree::frontier::Frontier;
5963 #[cfg(feature = "orchard")]
5964 use {
5965 crate::testing::db::TestDb, ::orchard::tree::MerkleHashOrchard,
5966 incrementalmerkletree::Hashable as _, shardtree::error::ShardTreeError,
5967 zcash_client_backend::data_api::WalletCommitmentTrees,
5968 zcash_protocol::local_consensus::LocalNetwork,
5969 };
5970
5971 fn connection_with_checkpoint_tables() -> Connection {
5972 let conn = Connection::open_in_memory().unwrap();
5973 conn.execute_batch(
5974 "CREATE TABLE blocks (height INTEGER PRIMARY KEY);
5975 CREATE TABLE transactions (id_tx INTEGER PRIMARY KEY, mined_height INTEGER);
5976 CREATE TABLE sapling_tree_checkpoints (checkpoint_id INTEGER PRIMARY KEY);
5977 CREATE TABLE orchard_tree_checkpoints (checkpoint_id INTEGER PRIMARY KEY);
5978 CREATE TABLE ironwood_tree_checkpoints (checkpoint_id INTEGER PRIMARY KEY);
5979 CREATE TABLE sapling_received_notes (
5980 id INTEGER PRIMARY KEY,
5981 transaction_id INTEGER,
5982 commitment_tree_position INTEGER);
5983 CREATE TABLE orchard_received_notes (
5984 id INTEGER PRIMARY KEY,
5985 transaction_id INTEGER,
5986 commitment_tree_position INTEGER);
5987 CREATE TABLE ironwood_received_notes (
5988 id INTEGER PRIMARY KEY,
5989 transaction_id INTEGER,
5990 commitment_tree_position INTEGER);",
5991 )
5992 .unwrap();
5993 conn
5994 }
5995
5996 #[test]
6000 fn truncation_height_tolerates_lagging_ironwood_checkpoints() {
6001 let mut conn = connection_with_checkpoint_tables();
6002 conn.execute_batch(
6003 "INSERT INTO blocks (height) VALUES (10), (11);
6004 INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6005 INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6006 INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (10);",
6007 )
6008 .unwrap();
6009
6010 let tx = conn.transaction().unwrap();
6011 assert_eq!(
6012 select_truncation_height(&tx, BlockHeight::from_u32(11)).unwrap(),
6013 BlockHeight::from_u32(11),
6014 );
6015 }
6016
6017 #[test]
6021 fn truncation_height_tolerates_tree_emptying_ironwood_truncation() {
6022 let mut conn = connection_with_checkpoint_tables();
6023 conn.execute_batch(
6024 "INSERT INTO blocks (height) VALUES (10), (11);
6025 INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6026 INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6027 INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (12), (13);",
6028 )
6029 .unwrap();
6030
6031 let tx = conn.transaction().unwrap();
6032 assert_eq!(
6033 select_truncation_height(&tx, BlockHeight::from_u32(11)).unwrap(),
6034 BlockHeight::from_u32(11),
6035 );
6036 }
6037
6038 #[test]
6042 fn truncation_height_rejects_straddling_ironwood_checkpoints() {
6043 let mut conn = connection_with_checkpoint_tables();
6044 conn.execute_batch(
6045 "INSERT INTO blocks (height) VALUES (9), (10), (11);
6046 INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (9), (10), (11);
6047 INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (9), (10), (11);
6048 INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (9), (11);",
6049 )
6050 .unwrap();
6051
6052 let tx = conn.transaction().unwrap();
6053 assert_eq!(
6054 select_truncation_height(&tx, BlockHeight::from_u32(10)).unwrap(),
6055 BlockHeight::from_u32(9),
6056 );
6057 }
6058
6059 #[test]
6063 fn truncation_height_rejects_witness_destroying_ironwood_truncation() {
6064 let mut conn = connection_with_checkpoint_tables();
6065 conn.execute_batch(
6066 "INSERT INTO blocks (height) VALUES (10), (11);
6067 INSERT INTO transactions (id_tx, mined_height) VALUES (1, 10);
6068 INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6069 INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6070 INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (12), (13);
6071 INSERT INTO ironwood_received_notes (id, transaction_id, commitment_tree_position)
6072 VALUES (1, 1, 5);",
6073 )
6074 .unwrap();
6075
6076 let tx = conn.transaction().unwrap();
6077 assert_matches!(
6078 select_truncation_height(&tx, BlockHeight::from_u32(11)),
6079 Err(SqliteClientError::RequestedRewindInvalid {
6080 safe_rewind_height: None,
6081 ..
6082 })
6083 );
6084 }
6085
6086 #[test]
6087 fn safe_rewind_height_requires_an_ironwood_checkpoint() {
6088 let conn = connection_with_checkpoint_tables();
6089 conn.execute_batch(
6090 "INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10);
6091 INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10);
6092 INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (11);",
6093 )
6094 .unwrap();
6095
6096 assert_eq!(min_shared_checkpoint_height(&conn).unwrap(), None);
6097 }
6098
6099 #[test]
6100 fn empty_database_has_no_balance() {
6101 let st = TestBuilder::new()
6102 .with_data_store_factory(TestDbFactory::default())
6103 .with_account_from_sapling_activation(BlockHash([0; 32]))
6104 .build();
6105 let account = st.test_account().unwrap();
6106
6107 assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6109
6110 assert_eq!(
6112 st.wallet()
6113 .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap())
6114 .unwrap(),
6115 None
6116 );
6117
6118 assert_matches!(
6120 st.wallet().get_last_generated_address_matching(
6121 account.id(),
6122 UnifiedAddressRequest::AllAvailableKeys
6123 ),
6124 Ok(Some(_))
6125 );
6126
6127 assert_matches!(
6129 st.wallet().get_last_generated_address_matching(
6130 AccountUuid(Uuid::nil()),
6131 UnifiedAddressRequest::AllAvailableKeys
6132 ),
6133 Err(SqliteClientError::AccountUnknown)
6134 );
6135 }
6136
6137 #[test]
6138 fn status_intent_persists_until_the_transaction_is_terminal() {
6139 const TEST_VALUE: Zatoshis = Zatoshis::const_from_u64(10_000);
6140 const FUTURE_EXPIRY_OFFSET: u32 = 10;
6141 const UNEXPIRED_TXID_BYTES: [u8; 32] = [1; 32];
6142 const EXPIRED_TXID_BYTES: [u8; 32] = [2; 32];
6143
6144 let mut st = TestBuilder::new()
6145 .with_data_store_factory(TestDbFactory::default())
6146 .with_block_cache(BlockCache::new())
6147 .with_account_from_sapling_activation(BlockHash([0; 32]))
6148 .build();
6149
6150 let dfvk = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
6151 let tip = st.sapling_activation_height();
6152 st.generate_block_at(
6153 tip,
6154 BlockHash([0; 32]),
6155 &[FakeCompactOutput::new(
6156 &dfvk,
6157 AddressType::DefaultExternal,
6158 TEST_VALUE,
6159 )],
6160 0,
6161 0,
6162 0,
6163 false,
6164 );
6165 st.scan_cached_blocks(tip, 1);
6166
6167 let unexpired_txid = TxId::from_bytes(UNEXPIRED_TXID_BYTES);
6168 let expired_txid = TxId::from_bytes(EXPIRED_TXID_BYTES);
6169 for (txid, expiry_height) in [
6170 (unexpired_txid, u32::from(tip) + FUTURE_EXPIRY_OFFSET),
6171 (expired_txid, u32::from(tip)),
6172 ] {
6173 st.wallet()
6174 .conn()
6175 .execute(
6176 "INSERT INTO transactions (txid, expiry_height, min_observed_height)
6177 VALUES (:txid, :expiry_height, :min_observed_height)",
6178 named_params![
6179 ":txid": txid.as_ref(),
6180 ":expiry_height": expiry_height,
6181 ":min_observed_height": u32::from(tip),
6182 ],
6183 )
6184 .unwrap();
6185 st.wallet()
6186 .conn()
6187 .execute(
6188 "INSERT INTO tx_retrieval_queue (txid, query_type)
6189 VALUES (:txid, :query_type)",
6190 named_params![
6191 ":txid": txid.as_ref(),
6192 ":query_type": TxQueryType::Status.code(),
6193 ],
6194 )
6195 .unwrap();
6196 }
6197
6198 for txid in [unexpired_txid, expired_txid] {
6199 st.wallet_mut()
6200 .set_transaction_status(txid, TransactionStatus::NotInMainChain)
6201 .unwrap();
6202 }
6203
6204 let requests = st.wallet().transaction_data_requests().unwrap();
6205 assert!(requests.contains(&TransactionDataRequest::GetStatus(unexpired_txid)));
6206 assert!(!requests.contains(&TransactionDataRequest::GetStatus(expired_txid)));
6207
6208 let db_tx = st.wallet().conn().unchecked_transaction().unwrap();
6209 queue_tx_retrieval(&db_tx, std::iter::once(unexpired_txid), None).unwrap();
6210 db_tx.commit().unwrap();
6211
6212 let requests = st.wallet().transaction_data_requests().unwrap();
6213 assert!(requests.contains(&TransactionDataRequest::GetStatus(unexpired_txid)));
6214 assert!(requests.contains(&TransactionDataRequest::Enhancement(unexpired_txid)));
6215 }
6216
6217 #[test]
6218 fn get_default_account_index() {
6219 let st = TestBuilder::new()
6220 .with_data_store_factory(TestDbFactory::default())
6221 .with_account_from_sapling_activation(BlockHash([0; 32]))
6222 .build();
6223 let account_id = st.test_account().unwrap().id();
6224 let account_parameters = st.wallet().get_account(account_id).unwrap().unwrap();
6225
6226 let expected_account_index = zip32::AccountId::try_from(0).unwrap();
6227 assert_matches!(
6228 account_parameters.kind,
6229 AccountSource::Derived{derivation, ..} if derivation.account_index() == expected_account_index
6230 );
6231 }
6232
6233 #[test]
6234 fn get_account_ids() {
6235 let mut st = TestBuilder::new()
6236 .with_data_store_factory(TestDbFactory::default())
6237 .with_account_from_sapling_activation(BlockHash([0; 32]))
6238 .build();
6239
6240 let seed = SecretVec::new(st.test_seed().unwrap().expose_secret().clone());
6241 let birthday = st.test_account().unwrap().birthday().clone();
6242
6243 st.wallet_mut()
6244 .create_account("", &seed, &birthday, None)
6245 .unwrap();
6246
6247 for acct_id in st.wallet().get_account_ids().unwrap() {
6248 assert_matches!(st.wallet().get_account(acct_id), Ok(Some(_)))
6249 }
6250 }
6251
6252 #[test]
6253 fn block_fully_scanned() {
6254 check_block_fully_scanned(TestDbFactory::default())
6255 }
6256
6257 fn check_block_fully_scanned<DsF: DataStoreFactory>(dsf: DsF) {
6258 let mut st = TestBuilder::new()
6259 .with_data_store_factory(dsf)
6260 .with_block_cache(BlockCache::new())
6261 .with_account_from_sapling_activation(BlockHash([0; 32]))
6262 .build();
6263
6264 let block_fully_scanned = |st: &TestState<_, DsF::DataStore, _>| {
6265 st.wallet()
6266 .block_fully_scanned()
6267 .unwrap()
6268 .map(|meta| meta.block_height())
6269 };
6270
6271 assert_eq!(block_fully_scanned(&st), None);
6273
6274 let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
6276 let not_our_value = Zatoshis::const_from_u64(10000);
6277 let start_height = st.sapling_activation_height();
6278 let _ = st.generate_block_at(
6279 start_height,
6280 BlockHash([0; 32]),
6281 &[FakeCompactOutput::new(
6282 ¬_our_key,
6283 AddressType::DefaultExternal,
6284 not_our_value,
6285 )],
6286 0,
6287 0,
6288 0,
6289 false,
6290 );
6291 let (mid_height, _, _) =
6292 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
6293 let (end_height, _, _) =
6294 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
6295
6296 st.scan_cached_blocks(end_height, 1);
6298
6299 assert_eq!(block_fully_scanned(&st), None);
6302
6303 st.scan_cached_blocks(start_height, 1);
6305
6306 assert_eq!(block_fully_scanned(&st), Some(start_height));
6308
6309 st.scan_cached_blocks(mid_height, 1);
6311
6312 assert_eq!(block_fully_scanned(&st), Some(end_height));
6315 }
6316
6317 #[test]
6318 fn test_account_birthday() {
6319 let st = TestBuilder::new()
6320 .with_data_store_factory(TestDbFactory::default())
6321 .with_block_cache(BlockCache::new())
6322 .with_account_from_sapling_activation(BlockHash([0; 32]))
6323 .build();
6324
6325 let account_id = st.test_account().unwrap().id();
6326 assert_matches!(
6327 account_birthday(st.wallet().conn(), account_id),
6328 Ok(birthday) if birthday == st.sapling_activation_height()
6329 )
6330 }
6331
6332 #[test]
6333 fn rewound_birthday_does_not_falsely_report_complete_recovery() {
6334 let prior_block_hash = BlockHash([0; 32]);
6340 let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
6341 let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
6342 let initial_height_offset: u32 = 310;
6343
6344 let mut st = TestBuilder::new()
6345 .with_data_store_factory(TestDbFactory::default())
6346 .with_block_cache(BlockCache::new())
6347 .with_initial_chain_state(|rng, network| {
6348 let sapling_activation_height =
6349 network.activation_height(NetworkUpgrade::Sapling).unwrap();
6350 let (prior_sapling_roots, sapling_initial_tree) =
6351 Frontier::random_with_prior_subtree_roots(
6352 rng,
6353 initial_sapling_tree_size.into(),
6354 NonZeroU8::new(16).unwrap(),
6355 );
6356 let prior_sapling_roots = prior_sapling_roots
6357 .into_iter()
6358 .zip(1u32..)
6359 .map(|(root, i)| {
6360 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6361 })
6362 .collect::<Vec<_>>();
6363
6364 #[cfg(feature = "orchard")]
6365 let (prior_orchard_roots, orchard_initial_tree) =
6366 Frontier::random_with_prior_subtree_roots(
6367 rng,
6368 initial_orchard_tree_size.into(),
6369 NonZeroU8::new(16).unwrap(),
6370 );
6371 #[cfg(feature = "orchard")]
6372 let prior_orchard_roots = prior_orchard_roots
6373 .into_iter()
6374 .zip(1u32..)
6375 .map(|(root, i)| {
6376 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6377 })
6378 .collect::<Vec<_>>();
6379
6380 #[cfg(feature = "orchard")]
6385 let ironwood_initial_tree = Frontier::empty();
6386
6387 InitialChainState {
6388 chain_state: ChainState::new(
6389 sapling_activation_height + initial_height_offset - 1,
6390 prior_block_hash,
6391 sapling_initial_tree,
6392 #[cfg(feature = "orchard")]
6393 orchard_initial_tree,
6394 #[cfg(feature = "orchard")]
6395 ironwood_initial_tree,
6396 ),
6397 prior_sapling_roots,
6398 #[cfg(feature = "orchard")]
6399 prior_orchard_roots,
6400 }
6401 })
6402 .with_account_having_current_birthday()
6403 .build();
6404
6405 let sapling_activation_height = st.sapling_activation_height();
6406 let dfvk = SaplingPoolTester::test_account_fvk(&st);
6407 let initial_height = sapling_activation_height + initial_height_offset;
6408
6409 st.generate_block_at(
6413 initial_height,
6414 prior_block_hash,
6415 &[FakeCompactOutput::new(
6416 &dfvk,
6417 AddressType::DefaultExternal,
6418 Zatoshis::const_from_u64(50000),
6419 )],
6420 initial_sapling_tree_size,
6421 initial_orchard_tree_size,
6422 0,
6423 false,
6424 );
6425 for _ in 1..10 {
6426 st.generate_next_block(
6427 &dfvk,
6428 AddressType::DefaultExternal,
6429 Zatoshis::const_from_u64(10000),
6430 );
6431 }
6432 st.scan_cached_blocks(initial_height, 10);
6433
6434 let chain_tip_height = initial_height + 9;
6435 let recover_until_height = initial_height + 5;
6436
6437 let progress = super::subtree_scan_progress(
6443 st.wallet().conn(),
6444 st.network(),
6445 ShieldedPool::Sapling,
6446 sapling_activation_height,
6447 sapling_activation_height,
6448 Some(recover_until_height),
6449 chain_tip_height,
6450 )
6451 .expect("subtree_scan_progress must not error")
6452 .expect("a Progress value should be returned");
6453
6454 let recovery = progress
6455 .recovery()
6456 .expect("recovery progress should be reported");
6457
6458 assert!(
6463 recovery.numerator() < recovery.denominator(),
6464 "recovery wrongly reports {n}/{d} after a rewind to a birthday \
6465 below all scanned blocks; at least {unscanned} outputs in \
6466 [{birthday:?}, {first:?}) live only in imported subtree roots and \
6467 have never been scanned",
6468 n = recovery.numerator(),
6469 d = recovery.denominator(),
6470 unscanned = u64::from(initial_sapling_tree_size),
6471 birthday = sapling_activation_height,
6472 first = initial_height,
6473 );
6474 }
6475
6476 #[test]
6477 fn rewound_birthday_recovery_denominator_includes_imported_subtrees() {
6478 let prior_block_hash = BlockHash([0; 32]);
6485 let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
6486 let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
6487 let initial_height_offset: u32 = 310;
6488
6489 let mut st = TestBuilder::new()
6490 .with_data_store_factory(TestDbFactory::default())
6491 .with_block_cache(BlockCache::new())
6492 .with_initial_chain_state(|rng, network| {
6493 let sapling_activation_height =
6494 network.activation_height(NetworkUpgrade::Sapling).unwrap();
6495 let (prior_sapling_roots, sapling_initial_tree) =
6496 Frontier::random_with_prior_subtree_roots(
6497 rng,
6498 initial_sapling_tree_size.into(),
6499 NonZeroU8::new(16).unwrap(),
6500 );
6501 let prior_sapling_roots = prior_sapling_roots
6502 .into_iter()
6503 .zip(1u32..)
6504 .map(|(root, i)| {
6505 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6506 })
6507 .collect::<Vec<_>>();
6508
6509 #[cfg(feature = "orchard")]
6510 let (prior_orchard_roots, orchard_initial_tree) =
6511 Frontier::random_with_prior_subtree_roots(
6512 rng,
6513 initial_orchard_tree_size.into(),
6514 NonZeroU8::new(16).unwrap(),
6515 );
6516 #[cfg(feature = "orchard")]
6517 let prior_orchard_roots = prior_orchard_roots
6518 .into_iter()
6519 .zip(1u32..)
6520 .map(|(root, i)| {
6521 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6522 })
6523 .collect::<Vec<_>>();
6524
6525 #[cfg(feature = "orchard")]
6530 let ironwood_initial_tree = Frontier::empty();
6531
6532 InitialChainState {
6533 chain_state: ChainState::new(
6534 sapling_activation_height + initial_height_offset - 1,
6535 prior_block_hash,
6536 sapling_initial_tree,
6537 #[cfg(feature = "orchard")]
6538 orchard_initial_tree,
6539 #[cfg(feature = "orchard")]
6540 ironwood_initial_tree,
6541 ),
6542 prior_sapling_roots,
6543 #[cfg(feature = "orchard")]
6544 prior_orchard_roots,
6545 }
6546 })
6547 .with_account_having_current_birthday()
6548 .build();
6549
6550 let sapling_activation_height = st.sapling_activation_height();
6551 let dfvk = SaplingPoolTester::test_account_fvk(&st);
6552 let initial_height = sapling_activation_height + initial_height_offset;
6553
6554 st.generate_block_at(
6555 initial_height,
6556 prior_block_hash,
6557 &[FakeCompactOutput::new(
6558 &dfvk,
6559 AddressType::DefaultExternal,
6560 Zatoshis::const_from_u64(50000),
6561 )],
6562 initial_sapling_tree_size,
6563 initial_orchard_tree_size,
6564 0,
6565 false,
6566 );
6567 for _ in 1..10 {
6568 st.generate_next_block(
6569 &dfvk,
6570 AddressType::DefaultExternal,
6571 Zatoshis::const_from_u64(10000),
6572 );
6573 }
6574 st.scan_cached_blocks(initial_height, 10);
6575
6576 let chain_tip_height = initial_height + 9;
6577 let recover_until_height = initial_height + 5;
6578
6579 let progress = super::subtree_scan_progress(
6580 st.wallet().conn(),
6581 st.network(),
6582 ShieldedPool::Sapling,
6583 sapling_activation_height,
6584 sapling_activation_height,
6585 Some(recover_until_height),
6586 chain_tip_height,
6587 )
6588 .expect("subtree_scan_progress must not error")
6589 .expect("a Progress value should be returned");
6590
6591 let recovery = progress
6592 .recovery()
6593 .expect("recovery progress should be reported");
6594
6595 assert!(
6597 recovery.numerator() <= recovery.denominator(),
6598 "recovery numerator {n} exceeds denominator {d} in the \
6599 rewound-birthday scenario",
6600 n = recovery.numerator(),
6601 d = recovery.denominator(),
6602 );
6603
6604 assert!(
6609 recovery.numerator() < recovery.denominator(),
6610 "recovery wrongly reports {n}/{d} after a rewind to a birthday \
6611 below all scanned blocks; at least {unscanned} outputs in \
6612 [{birthday:?}, {first:?}) live only in imported subtree roots \
6613 and have never been scanned",
6614 n = recovery.numerator(),
6615 d = recovery.denominator(),
6616 unscanned = u64::from(initial_sapling_tree_size),
6617 birthday = sapling_activation_height,
6618 first = initial_height,
6619 );
6620
6621 assert!(
6625 *recovery.denominator() >= u64::from(initial_sapling_tree_size),
6626 "recovery denominator {d} fails to account for the {imported} \
6627 outputs of the imported subtree roots that fall within \
6628 [{birthday:?}, {recover:?})",
6629 d = recovery.denominator(),
6630 imported = u64::from(initial_sapling_tree_size),
6631 birthday = sapling_activation_height,
6632 recover = recover_until_height,
6633 );
6634 }
6635
6636 #[test]
6637 fn recover_until_above_chain_tip_does_not_overshoot_tip_size() {
6638 let prior_block_hash = BlockHash([0; 32]);
6647 let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
6648 let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
6649 let initial_height_offset: u32 = 310;
6650
6651 let mut st = TestBuilder::new()
6652 .with_data_store_factory(TestDbFactory::default())
6653 .with_block_cache(BlockCache::new())
6654 .with_initial_chain_state(|rng, network| {
6655 let sapling_activation_height =
6656 network.activation_height(NetworkUpgrade::Sapling).unwrap();
6657 let (prior_sapling_roots, sapling_initial_tree) =
6658 Frontier::random_with_prior_subtree_roots(
6659 rng,
6660 initial_sapling_tree_size.into(),
6661 NonZeroU8::new(16).unwrap(),
6662 );
6663 let prior_sapling_roots = prior_sapling_roots
6664 .into_iter()
6665 .zip(1u32..)
6666 .map(|(root, i)| {
6667 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6668 })
6669 .collect::<Vec<_>>();
6670
6671 #[cfg(feature = "orchard")]
6672 let (prior_orchard_roots, orchard_initial_tree) =
6673 Frontier::random_with_prior_subtree_roots(
6674 rng,
6675 initial_orchard_tree_size.into(),
6676 NonZeroU8::new(16).unwrap(),
6677 );
6678 #[cfg(feature = "orchard")]
6679 let prior_orchard_roots = prior_orchard_roots
6680 .into_iter()
6681 .zip(1u32..)
6682 .map(|(root, i)| {
6683 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6684 })
6685 .collect::<Vec<_>>();
6686
6687 #[cfg(feature = "orchard")]
6692 let ironwood_initial_tree = Frontier::empty();
6693
6694 InitialChainState {
6695 chain_state: ChainState::new(
6696 sapling_activation_height + initial_height_offset - 1,
6697 prior_block_hash,
6698 sapling_initial_tree,
6699 #[cfg(feature = "orchard")]
6700 orchard_initial_tree,
6701 #[cfg(feature = "orchard")]
6702 ironwood_initial_tree,
6703 ),
6704 prior_sapling_roots,
6705 #[cfg(feature = "orchard")]
6706 prior_orchard_roots,
6707 }
6708 })
6709 .with_account_having_current_birthday()
6710 .build();
6711
6712 let sapling_activation_height = st.sapling_activation_height();
6713 let dfvk = SaplingPoolTester::test_account_fvk(&st);
6714 let initial_height = sapling_activation_height + initial_height_offset;
6715
6716 st.generate_block_at(
6717 initial_height,
6718 prior_block_hash,
6719 &[FakeCompactOutput::new(
6720 &dfvk,
6721 AddressType::DefaultExternal,
6722 Zatoshis::const_from_u64(50000),
6723 )],
6724 initial_sapling_tree_size,
6725 initial_orchard_tree_size,
6726 0,
6727 false,
6728 );
6729 for _ in 1..10 {
6730 st.generate_next_block(
6731 &dfvk,
6732 AddressType::DefaultExternal,
6733 Zatoshis::const_from_u64(10000),
6734 );
6735 }
6736 st.scan_cached_blocks(initial_height, 10);
6737
6738 let chain_tip_height = initial_height + 9;
6739 let recover_until_height = chain_tip_height + 5;
6743
6744 let progress = super::subtree_scan_progress(
6749 st.wallet().conn(),
6750 st.network(),
6751 ShieldedPool::Sapling,
6752 sapling_activation_height,
6753 sapling_activation_height,
6754 Some(recover_until_height),
6755 chain_tip_height,
6756 )
6757 .expect("subtree_scan_progress must not error")
6758 .expect("a Progress value should be returned");
6759
6760 let scan = progress.scan();
6761
6762 assert!(
6765 *scan.denominator() <= u64::from(initial_sapling_tree_size) + 1_000,
6766 "scan denominator {d} appears to have underflowed (raw u64); \
6767 tip_tree_size and recover_until_size disagree about which is \
6768 upper-bound",
6769 d = scan.denominator(),
6770 );
6771 assert!(
6773 scan.numerator() <= scan.denominator(),
6774 "scan numerator {n} exceeds denominator {d}",
6775 n = scan.numerator(),
6776 d = scan.denominator(),
6777 );
6778 }
6779
6780 #[test]
6784 fn rewind_to_chain_state_below_all_birthdays_with_empty_reset_returns_error() {
6785 let mut st = TestBuilder::new()
6786 .with_data_store_factory(TestDbFactory::default())
6787 .with_account_from_sapling_activation(BlockHash([0; 32]))
6788 .build();
6789
6790 let account_id = st.test_account().unwrap().id();
6791 let original_birthday = st.test_account().unwrap().birthday().height();
6792 let target_height = original_birthday - 10;
6796
6797 let result = st.wallet_mut().rewind_to_chain_state(
6798 ChainState::empty(target_height, BlockHash([0; 32])),
6799 HashSet::new(),
6800 );
6801
6802 assert_matches!(
6803 result,
6804 Err(RewindError::RewindBeyondBirthdays(birthdays))
6805 if birthdays.get(&account_id) == Some(&original_birthday)
6806 );
6807 }
6808
6809 #[test]
6813 fn rewind_to_chain_state_below_all_birthdays_with_account_in_reset_succeeds() {
6814 let mut st = TestBuilder::new()
6815 .with_data_store_factory(TestDbFactory::default())
6816 .with_account_from_sapling_activation(BlockHash([0; 32]))
6817 .build();
6818
6819 let account_id = st.test_account().unwrap().id();
6820 let original_birthday = st.test_account().unwrap().birthday().height();
6821 let target_height = original_birthday - 10;
6825
6826 st.wallet_mut()
6827 .rewind_to_chain_state(
6828 ChainState::empty(target_height, BlockHash([0; 32])),
6829 HashSet::from([account_id]),
6830 )
6831 .expect("rewind_to_chain_state should succeed when the account is in reset");
6832
6833 assert_matches!(
6835 account_birthday(st.wallet().conn(), account_id),
6836 Ok(b) if b == target_height + 1
6837 );
6838 }
6839
6840 #[test]
6844 fn rewind_to_chain_state_with_unknown_uuid_in_reset_returns_data_source_error() {
6845 let mut st = TestBuilder::new()
6846 .with_data_store_factory(TestDbFactory::default())
6847 .with_account_from_sapling_activation(BlockHash([0; 32]))
6848 .build();
6849
6850 let original_birthday = st.test_account().unwrap().birthday().height();
6851 let target_height = original_birthday - 10;
6855
6856 let bogus_uuid = AccountUuid(Uuid::from_u128(0xDEADBEEF));
6857 let result = st.wallet_mut().rewind_to_chain_state(
6858 ChainState::empty(target_height, BlockHash([0; 32])),
6859 HashSet::from([bogus_uuid]),
6860 );
6861
6862 assert_matches!(
6863 result,
6864 Err(RewindError::DataSource(SqliteClientError::CorruptedData(_)))
6865 );
6866 }
6867
6868 #[cfg(feature = "orchard")]
6874 fn wallet_with_scanned_blocks() -> (TestState<BlockCache, TestDb, LocalNetwork>, BlockHeight) {
6875 let mut st = TestBuilder::new()
6876 .with_data_store_factory(TestDbFactory::default())
6877 .with_block_cache(BlockCache::new())
6878 .with_account_from_sapling_activation(BlockHash([0; 32]))
6879 .build();
6880
6881 let dfvk = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
6882 let value = Zatoshis::const_from_u64(10000);
6883 let start_height = st.sapling_activation_height();
6884
6885 st.generate_block_at(
6886 start_height,
6887 BlockHash([0; 32]),
6888 &[FakeCompactOutput::new(
6889 &dfvk,
6890 AddressType::DefaultExternal,
6891 value,
6892 )],
6893 0,
6894 0,
6895 0,
6896 false,
6897 );
6898 for _ in 1..5 {
6899 st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6900 }
6901 st.scan_cached_blocks(start_height, 5);
6902
6903 (st, start_height)
6904 }
6905
6906 #[cfg(feature = "orchard")]
6907 fn table_row_count(conn: &Connection, table: &str) -> u32 {
6908 conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| {
6909 row.get(0)
6910 })
6911 .unwrap()
6912 }
6913
6914 #[cfg(feature = "orchard")]
6915 fn max_block_height(conn: &Connection) -> Option<BlockHeight> {
6916 conn.query_row("SELECT MAX(height) FROM blocks", [], |row| {
6917 row.get::<_, Option<u32>>(0)
6918 })
6919 .unwrap()
6920 .map(BlockHeight::from)
6921 }
6922
6923 #[cfg(feature = "orchard")]
6924 fn rescan_queued_from(conn: &Connection, height: BlockHeight) -> bool {
6925 conn.query_row(
6926 "SELECT EXISTS(SELECT 1 FROM scan_queue WHERE block_range_start = ?)",
6927 [u32::from(height)],
6928 |row| row.get(0),
6929 )
6930 .unwrap()
6931 }
6932
6933 #[test]
6939 #[cfg(feature = "orchard")]
6940 fn rewind_to_chain_state_with_empty_ironwood_tree_succeeds() {
6941 let (mut st, start_height) = wallet_with_scanned_blocks();
6942
6943 st.wallet()
6946 .conn()
6947 .execute_batch(
6948 "DELETE FROM ironwood_tree_checkpoints;
6949 DELETE FROM ironwood_tree_shards;
6950 DELETE FROM ironwood_tree_cap;",
6951 )
6952 .unwrap();
6953
6954 let target_height = start_height + 2;
6955 let result = st.wallet_mut().rewind_to_chain_state(
6956 ChainState::empty(target_height, BlockHash([0; 32])),
6957 HashSet::new(),
6958 );
6959 assert_matches!(result, Ok(()));
6960
6961 assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
6965 assert!(rescan_queued_from(st.wallet().conn(), target_height + 1));
6966 assert_eq!(
6967 table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
6968 0
6969 );
6970 assert_eq!(
6971 table_row_count(st.wallet().conn(), "ironwood_tree_shards"),
6972 0
6973 );
6974 }
6975
6976 #[test]
6980 #[cfg(feature = "orchard")]
6981 fn rewind_to_chain_state_with_empty_orchard_tree_succeeds() {
6982 let (mut st, start_height) = wallet_with_scanned_blocks();
6983
6984 st.wallet()
6987 .conn()
6988 .execute_batch(
6989 "DELETE FROM orchard_tree_checkpoints;
6990 DELETE FROM orchard_tree_shards;
6991 DELETE FROM orchard_tree_cap;",
6992 )
6993 .unwrap();
6994
6995 let target_height = start_height + 2;
6996 let result = st.wallet_mut().rewind_to_chain_state(
6997 ChainState::empty(target_height, BlockHash([0; 32])),
6998 HashSet::new(),
6999 );
7000 assert_matches!(result, Ok(()));
7001
7002 assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7003 assert_eq!(
7004 table_row_count(st.wallet().conn(), "orchard_tree_checkpoints"),
7005 0
7006 );
7007 }
7008
7009 #[test]
7014 #[cfg(feature = "orchard")]
7015 fn rewind_to_chain_state_with_straddling_ironwood_checkpoints_errors() {
7016 let (mut st, start_height) = wallet_with_scanned_blocks();
7017 let target_height = start_height + 2;
7018
7019 st.wallet()
7023 .conn()
7024 .execute(
7025 "DELETE FROM ironwood_tree_checkpoints WHERE checkpoint_id = ?",
7026 [u32::from(target_height)],
7027 )
7028 .unwrap();
7029
7030 let result = st.wallet_mut().rewind_to_chain_state(
7031 ChainState::empty(target_height, BlockHash([0; 32])),
7032 HashSet::new(),
7033 );
7034
7035 assert_matches!(
7036 result,
7037 Err(RewindError::DataSource(SqliteClientError::CorruptedData(_)))
7038 );
7039 }
7040
7041 #[test]
7047 #[cfg(feature = "orchard")]
7048 fn rewind_to_chain_state_with_lagging_ironwood_tree_succeeds() {
7049 let (mut st, start_height) = wallet_with_scanned_blocks();
7050
7051 let ironwood_lag_height = start_height + 1;
7055 st.wallet_mut()
7056 .with_ironwood_tree_mut(|tree| {
7057 assert!(tree.truncate_to_checkpoint(&ironwood_lag_height)?);
7058 Ok::<_, ShardTreeError<crate::wallet::commitment_tree::Error>>(())
7059 })
7060 .unwrap();
7061
7062 let target_height = start_height + 2;
7063 let result = st.wallet_mut().rewind_to_chain_state(
7064 ChainState::empty(target_height, BlockHash([0; 32])),
7065 HashSet::new(),
7066 );
7067 assert_matches!(result, Ok(()));
7068
7069 assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7072 assert_eq!(
7073 st.wallet()
7074 .conn()
7075 .query_row(
7076 "SELECT MAX(checkpoint_id) FROM ironwood_tree_checkpoints",
7077 [],
7078 |row| row.get::<_, Option<u32>>(0),
7079 )
7080 .unwrap()
7081 .map(BlockHeight::from),
7082 Some(ironwood_lag_height),
7083 );
7084 }
7085
7086 #[test]
7094 #[cfg(feature = "orchard")]
7095 fn rewind_to_chain_state_with_tip_only_ironwood_tree_empties_it() {
7096 let (mut st, start_height) = wallet_with_scanned_blocks();
7097 let target_height = start_height + 2;
7098
7099 st.wallet()
7102 .conn()
7103 .execute(
7104 "DELETE FROM ironwood_tree_checkpoints WHERE checkpoint_id <= ?",
7105 [u32::from(target_height)],
7106 )
7107 .unwrap();
7108
7109 let result = st.wallet_mut().rewind_to_chain_state(
7110 ChainState::empty(target_height, BlockHash([0; 32])),
7111 HashSet::new(),
7112 );
7113 assert_matches!(result, Ok(()));
7114
7115 assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7118 assert!(rescan_queued_from(st.wallet().conn(), target_height + 1));
7119 assert_eq!(
7120 table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
7121 0
7122 );
7123 assert_eq!(
7124 table_row_count(st.wallet().conn(), "ironwood_tree_shards"),
7125 0
7126 );
7127 assert_eq!(table_row_count(st.wallet().conn(), "ironwood_tree_cap"), 0);
7128 }
7129
7130 #[test]
7136 #[cfg(feature = "orchard")]
7137 fn rewind_to_chain_state_with_witness_destroying_truncation_errors() {
7138 let mut st = TestBuilder::new()
7139 .with_data_store_factory(TestDbFactory::default())
7140 .with_block_cache(BlockCache::new())
7141 .with_account_from_sapling_activation(BlockHash([0; 32]))
7142 .build();
7143
7144 let dfvk = st.test_account_sapling().unwrap().clone();
7147 let value = Zatoshis::const_from_u64(10000);
7148 let start_height = st.sapling_activation_height();
7149 st.generate_block_at(
7150 start_height,
7151 BlockHash([0; 32]),
7152 &[FakeCompactOutput::new(
7153 &dfvk,
7154 AddressType::DefaultExternal,
7155 value,
7156 )],
7157 0,
7158 0,
7159 0,
7160 false,
7161 );
7162 for _ in 1..5 {
7163 st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
7164 }
7165 st.scan_cached_blocks(start_height, 5);
7166
7167 let target_height = start_height + 2;
7171 st.wallet()
7172 .conn()
7173 .execute(
7174 "DELETE FROM sapling_tree_checkpoints WHERE checkpoint_id <= ?",
7175 [u32::from(target_height)],
7176 )
7177 .unwrap();
7178
7179 let result = st.wallet_mut().rewind_to_chain_state(
7180 ChainState::empty(target_height, BlockHash([0; 32])),
7181 HashSet::new(),
7182 );
7183
7184 assert_matches!(
7185 result,
7186 Err(RewindError::DataSource(
7187 SqliteClientError::RequestedRewindInvalid { .. }
7188 ))
7189 );
7190 }
7191
7192 #[test]
7197 #[cfg(feature = "orchard")]
7198 fn rewind_preserves_ironwood_subtree_roots_at_or_below_target() {
7199 let (mut st, start_height) = wallet_with_scanned_blocks();
7200 let target_height = start_height + 2;
7201
7202 st.wallet()
7206 .conn()
7207 .execute_batch(
7208 "DELETE FROM ironwood_tree_checkpoints;
7209 DELETE FROM ironwood_tree_shards;
7210 DELETE FROM ironwood_tree_cap;",
7211 )
7212 .unwrap();
7213 st.wallet_mut()
7214 .put_ironwood_subtree_roots(
7215 0,
7216 &[CommitmentTreeRoot::from_parts(
7217 start_height,
7218 MerkleHashOrchard::empty_leaf(),
7219 )],
7220 )
7221 .unwrap();
7222 st.wallet()
7223 .conn()
7224 .execute(
7225 "INSERT INTO ironwood_tree_checkpoints (checkpoint_id, position)
7226 VALUES (?, NULL)",
7227 [u32::from(target_height + 1)],
7228 )
7229 .unwrap();
7230
7231 let result = st.wallet_mut().rewind_to_chain_state(
7232 ChainState::empty(target_height, BlockHash([0; 32])),
7233 HashSet::new(),
7234 );
7235 assert_matches!(result, Ok(()));
7236
7237 assert_eq!(
7240 table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
7241 0
7242 );
7243 assert_eq!(
7244 st.wallet()
7245 .conn()
7246 .query_row(
7247 "SELECT shard_index, subtree_end_height, root_hash IS NOT NULL
7248 FROM ironwood_tree_shards",
7249 [],
7250 |row| {
7251 Ok((
7252 row.get::<_, u64>(0)?,
7253 row.get::<_, u32>(1)?,
7254 row.get::<_, bool>(2)?,
7255 ))
7256 },
7257 )
7258 .unwrap(),
7259 (0, u32::from(start_height), true),
7260 );
7261 assert_eq!(table_row_count(st.wallet().conn(), "ironwood_tree_cap"), 1);
7262 }
7263
7264 #[test]
7268 #[cfg(feature = "orchard")]
7269 fn truncate_to_height_with_tip_only_ironwood_tree_empties_it() {
7270 let (mut st, start_height) = wallet_with_scanned_blocks();
7271 let target_height = start_height + 2;
7272
7273 st.wallet()
7274 .conn()
7275 .execute(
7276 "DELETE FROM ironwood_tree_checkpoints WHERE checkpoint_id <= ?",
7277 [u32::from(target_height)],
7278 )
7279 .unwrap();
7280
7281 let result = st.wallet_mut().truncate_to_height(target_height);
7282 assert_matches!(result, Ok(h) if h == target_height);
7283
7284 assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7285 assert_eq!(
7286 table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
7287 0
7288 );
7289 assert_eq!(
7290 table_row_count(st.wallet().conn(), "ironwood_tree_shards"),
7291 0
7292 );
7293 }
7294
7295 fn received_notes_table(pool: ShieldedPool) -> &'static str {
7299 match pool {
7300 ShieldedPool::Sapling => "sapling_received_notes",
7301 #[cfg(feature = "orchard")]
7302 ShieldedPool::Orchard => "orchard_received_notes",
7303 #[cfg(feature = "orchard")]
7304 ShieldedPool::Ironwood => "ironwood_received_notes",
7305 #[cfg(not(feature = "orchard"))]
7306 other => panic!("pool {other:?} is unsupported without the `orchard` feature"),
7307 }
7308 }
7309
7310 fn seed_unflagged_received_note(
7317 conn: &rusqlite::Connection,
7318 pool: ShieldedPool,
7319 receiving_account: i64,
7320 funding_account: i64,
7321 key_scope: KeyScope,
7322 ) -> i64 {
7323 const TX_ROW_ID: i64 = 1;
7326 const TXID: [u8; 32] = [7; 32];
7327 const OBSERVED_HEIGHT: i64 = 0;
7328 const OUTPUT_INDEX: i64 = 0;
7329 const DIVERSIFIER: [u8; 11] = [0; 11];
7330 const NOTE_VALUE_ZATS: i64 = 1;
7331 const NOTE_COMPONENT: [u8; 32] = [0; 32];
7332 #[cfg(feature = "orchard")]
7335 const NOTE_VERSION: i64 = 2;
7336 const SENT_OUTPUT_POOL: i64 = 0;
7339
7340 conn.execute(
7341 "INSERT INTO transactions (id_tx, txid, min_observed_height)
7342 VALUES (:id_tx, :txid, :min_observed_height)",
7343 named_params! {
7344 ":id_tx": TX_ROW_ID,
7345 ":txid": &TXID[..],
7346 ":min_observed_height": OBSERVED_HEIGHT,
7347 },
7348 )
7349 .unwrap();
7350
7351 match pool {
7352 ShieldedPool::Sapling => {
7353 conn.execute(
7354 "INSERT INTO sapling_received_notes
7355 (transaction_id, output_index, account_id, diversifier, value, rcm,
7356 is_change, recipient_key_scope)
7357 VALUES (:tx, :output_index, :account, :diversifier, :value,
7358 :note_component, :is_change, :key_scope)",
7359 named_params! {
7360 ":tx": TX_ROW_ID,
7361 ":output_index": OUTPUT_INDEX,
7362 ":account": receiving_account,
7363 ":diversifier": &DIVERSIFIER[..],
7364 ":value": NOTE_VALUE_ZATS,
7365 ":note_component": &NOTE_COMPONENT[..],
7366 ":is_change": false,
7367 ":key_scope": key_scope.encode(),
7368 },
7369 )
7370 .unwrap();
7371 }
7372 #[cfg(feature = "orchard")]
7374 ShieldedPool::Orchard | ShieldedPool::Ironwood => {
7375 conn.execute(
7376 &format!(
7377 "INSERT INTO {} (transaction_id, action_index, account_id, diversifier,
7378 value, rho, rseed, note_version, is_change,
7379 recipient_key_scope)
7380 VALUES (:tx, :output_index, :account, :diversifier, :value,
7381 :note_component, :note_component, :note_version, :is_change,
7382 :key_scope)",
7383 received_notes_table(pool)
7384 ),
7385 named_params! {
7386 ":tx": TX_ROW_ID,
7387 ":output_index": OUTPUT_INDEX,
7388 ":account": receiving_account,
7389 ":diversifier": &DIVERSIFIER[..],
7390 ":value": NOTE_VALUE_ZATS,
7391 ":note_component": &NOTE_COMPONENT[..],
7392 ":note_version": NOTE_VERSION,
7393 ":is_change": false,
7394 ":key_scope": key_scope.encode(),
7395 },
7396 )
7397 .unwrap();
7398 }
7399 #[cfg(not(feature = "orchard"))]
7400 other => panic!("pool {other:?} is unsupported without the `orchard` feature"),
7401 }
7402
7403 conn.execute(
7404 "INSERT INTO sent_notes
7405 (transaction_id, output_pool, output_index, from_account_id, value)
7406 VALUES (:tx, :output_pool, :output_index, :from_account, :value)",
7407 named_params! {
7408 ":tx": TX_ROW_ID,
7409 ":output_pool": SENT_OUTPUT_POOL,
7410 ":output_index": OUTPUT_INDEX,
7411 ":from_account": funding_account,
7412 ":value": NOTE_VALUE_ZATS,
7413 },
7414 )
7415 .unwrap();
7416
7417 TX_ROW_ID
7418 }
7419
7420 fn only_account_id(conn: &rusqlite::Connection) -> i64 {
7421 conn.query_row("SELECT id FROM accounts", [], |row| row.get::<_, i64>(0))
7422 .unwrap()
7423 }
7424
7425 fn is_change(conn: &rusqlite::Connection, pool: ShieldedPool) -> bool {
7426 conn.query_row(
7427 &format!("SELECT is_change FROM {}", received_notes_table(pool)),
7428 [],
7429 |row| row.get::<_, bool>(0),
7430 )
7431 .unwrap()
7432 }
7433
7434 fn assert_internal_scope_note_becomes_change(pool: ShieldedPool) {
7446 let mut st = TestBuilder::new()
7447 .with_data_store_factory(TestDbFactory::default())
7448 .with_account_from_sapling_activation(BlockHash([0; 32]))
7449 .build();
7450
7451 let account_id = only_account_id(st.wallet().conn());
7452 let tx = st.wallet_mut().conn_mut().transaction().unwrap();
7453
7454 let tx_row_id =
7455 seed_unflagged_received_note(&tx, pool, account_id, account_id, KeyScope::INTERNAL);
7456 assert!(
7457 !is_change(&tx, pool),
7458 "{pool:?}: precondition, the note starts out unflagged"
7459 );
7460
7461 flag_previously_received_change(&tx, TxRef(tx_row_id)).unwrap();
7462
7463 assert!(
7464 is_change(&tx, pool),
7465 "{pool:?}: an internal-scope note in a self-funded transaction must be flagged \
7466 as change"
7467 );
7468 }
7469
7470 #[test]
7471 fn flags_previously_received_sapling_change() {
7472 assert_internal_scope_note_becomes_change(ShieldedPool::Sapling);
7473 }
7474
7475 #[test]
7476 #[cfg(feature = "orchard")]
7477 fn flags_previously_received_orchard_change() {
7478 assert_internal_scope_note_becomes_change(ShieldedPool::Orchard);
7479 }
7480
7481 #[test]
7482 #[cfg(feature = "orchard")]
7483 fn flags_previously_received_ironwood_change() {
7484 assert_internal_scope_note_becomes_change(ShieldedPool::Ironwood);
7485 }
7486
7487 #[test]
7492 #[cfg(feature = "orchard")]
7493 fn does_not_flag_external_scope_notes_as_change() {
7494 let pool = ShieldedPool::Ironwood;
7495 let mut st = TestBuilder::new()
7496 .with_data_store_factory(TestDbFactory::default())
7497 .with_account_from_sapling_activation(BlockHash([0; 32]))
7498 .build();
7499
7500 let account_id = only_account_id(st.wallet().conn());
7501 let tx = st.wallet_mut().conn_mut().transaction().unwrap();
7502
7503 let tx_row_id =
7504 seed_unflagged_received_note(&tx, pool, account_id, account_id, KeyScope::EXTERNAL);
7505
7506 flag_previously_received_change(&tx, TxRef(tx_row_id)).unwrap();
7507
7508 assert!(
7509 !is_change(&tx, pool),
7510 "an external-scope note must not be reclassified as change"
7511 );
7512 }
7513}