Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions zcash_client_sqlite/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ workspace.
- `impl<'conn, P, CL, R> WalletWrite for WalletDb<SqlTransaction<'conn>, P, CL, R>` to
enable calling `WalletWrite` methods inside `WalletDb::transactionally` (amortizing the
database transaction overhead).
- `WalletDb::get_unspent_orchard_notes_at_historical_height` returns all Orchard
notes that existed and were unspent at a given height.

### Changed
- The `accounts` table now stores IVK item caches instead of FVK item caches for
Expand Down
30 changes: 30 additions & 0 deletions zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,36 @@ impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clo
}
}

#[cfg(feature = "orchard")]
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletDb<C, P, CL, R> {
/// Return all Orchard notes received at or before `height`
/// and unspent as of that height, for the given account.
///
/// Unlike [`InputSource::select_unspent_notes`] (which applies confirmation,
/// dust, and expiry filters for transaction construction), this returns every
/// note that existed and was unspent at the given height.
///
/// This function does not verify that a Merkle witness can be constructed
/// for each returned note at `height`. Witness construction is a separate
/// concern intended to be handled by the callers. As an example, a companion
/// `WalletDb::generate_orchard_witnesses_at_historical_height` returns an
/// actionable error for any position the wallet cannot witness at `height`
/// (for example, because the wallet has not synced through `height`, the checkpoint was pruned,
/// or the position does not belong to the wallet).
pub fn get_unspent_orchard_notes_at_historical_height(
&self,
account: AccountUuid,
height: BlockHeight,
) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
wallet::orchard::get_unspent_orchard_notes_at_historical_height(
self.conn.borrow(),
&self.params,
account,
height,
)
}
}

/// A handle for the SQLite block source.
pub struct BlockDb(rusqlite::Connection);

Expand Down
144 changes: 144 additions & 0 deletions zcash_client_sqlite/src/wallet/orchard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,74 @@ pub(crate) fn select_spendable_orchard_notes<P: consensus::Parameters>(
)
}

/// Return all Orchard notes that were received at or before `height`
/// and unspent as of `height`, for the given account.
///
/// Unlike `select_spendable_notes` (which applies confirmation, dust, and
/// expiry filters for transaction construction), this returns every note
/// that existed and was unspent at the given height.
///
/// Height filtering uses `transactions.mined_height`, not `transactions.block`.
/// A transaction is considered to have occurred at its mined height as soon
/// as the wallet learns of that height (for example, from transparent UTXO
/// retrieval), even if the containing compact block has not been fully
/// scanned. In practice the two columns are equivalent for the notes this
/// query can return, because `nf IS NOT NULL` and
/// `commitment_tree_position IS NOT NULL` already require a scan of the
/// block that contains the receiving transaction.
///
/// This function does not verify that a Merkle witness can be constructed
/// for each returned note at `height`. Witness construction is a separate
/// concern intended to be handled by the callers. As an example, a companion
/// `WalletDb::generate_orchard_witnesses_at_historical_height` returns an
/// actionable error for any position the wallet cannot witness at `height`
/// (for example, because the wallet has not synced through `height`, the checkpoint was pruned,
/// or the position does not belong to the wallet).
pub(crate) fn get_unspent_orchard_notes_at_historical_height<P: consensus::Parameters>(
Comment thread
nuttycom marked this conversation as resolved.
conn: &Connection,
params: &P,
account: AccountUuid,
height: BlockHeight,
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
let external_scope = KeyScope::EXTERNAL.encode();
let internal_scope = KeyScope::INTERNAL.encode();

let mut stmt = conn.prepare_cached(&format!(
"SELECT
rn.id AS id, t.txid, rn.action_index,
rn.diversifier, rn.value, rn.rho, rn.rseed, rn.commitment_tree_position,
accounts.ufvk AS ufvk, rn.recipient_key_scope,
t.mined_height,
NULL AS max_shielding_input_height
FROM orchard_received_notes rn
INNER JOIN accounts ON accounts.id = rn.account_id
INNER JOIN transactions t ON t.id_tx = rn.transaction_id
WHERE accounts.uuid = :account_uuid
AND t.mined_height <= :height
AND rn.nf IS NOT NULL
AND rn.commitment_tree_position IS NOT NULL
AND rn.recipient_key_scope IN ({external_scope}, {internal_scope})
AND accounts.ufvk IS NOT NULL
AND rn.id NOT IN (
SELECT rns.orchard_received_note_id
FROM orchard_received_note_spends rns
JOIN transactions t_spend ON t_spend.id_tx = rns.transaction_id
WHERE t_spend.mined_height <= :height
)
ORDER BY rn.commitment_tree_position",
))?;

let rows = stmt.query_and_then(
named_params![
":account_uuid": account.0,
":height": u32::from(height),
],
|row| to_received_note(params, row),
)?;

rows.filter_map(|r| r.transpose()).collect()
}

pub(crate) fn ensure_address<
T: ReceivedOrchardOutput<AccountId = AccountUuid>,
P: consensus::Parameters,
Expand Down Expand Up @@ -639,4 +707,80 @@ pub(crate) mod tests {
fn coinbase_only_filtering() {
testing::pool::coinbase_only_filtering::<OrchardPoolTester>();
}

#[test]
#[cfg(feature = "orchard")]
fn get_unspent_orchard_notes_at_historical_height_boundary_heights() {
use zcash_client_backend::data_api::Account;
use zcash_client_backend::data_api::testing::{
AddressType, TestBuilder, pool::ShieldedPoolTester,
};
use zcash_primitives::block::BlockHash;
use zcash_protocol::value::Zatoshis;

use crate::testing::{BlockCache, db::TestDbFactory};

let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();

let account = st.test_account().cloned().unwrap();
let dfvk = OrchardPoolTester::test_account_fvk(&st);

// Receive a note at h1
let value = Zatoshis::const_from_u64(50000);
let (h1, _, nf) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
st.scan_cached_blocks(h1, 1);

// Spend that note at h2 (produces change back to us)
let not_our_key = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
let to = OrchardPoolTester::fvk_default_address(&not_our_key);
let spend_value = Zatoshis::const_from_u64(20000);
let (h2, _) = st.generate_next_block_spending(&dfvk, (nf, value), to, spend_value);
st.scan_cached_blocks(h2, 1);

// Receive another note at h3
let value3 = Zatoshis::const_from_u64(70000);
let (h3, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value3);
st.scan_cached_blocks(h3, 1);

let db = st.wallet().db();

// Before any notes: nothing (h1 - 1 is before the note was mined)
let notes = db
.get_unspent_orchard_notes_at_historical_height(account.id(), h1 - 1)
.unwrap();
assert_eq!(notes.len(), 0);

// At h1: original note received and unspent
let notes = db
.get_unspent_orchard_notes_at_historical_height(account.id(), h1)
.unwrap();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].note_value().unwrap(), value);

// At h2: original spent, only change note remains
let notes = db
.get_unspent_orchard_notes_at_historical_height(account.id(), h2)
.unwrap();
assert_eq!(notes.len(), 1);
assert_eq!(
notes[0].note_value().unwrap(),
(value - spend_value).unwrap()
);

// At h3: change note + new note
let notes = db
.get_unspent_orchard_notes_at_historical_height(account.id(), h3)
.unwrap();
assert_eq!(notes.len(), 2);
let total: Zatoshis = notes
.iter()
.map(|n| n.note_value().unwrap())
.sum::<Option<Zatoshis>>()
.unwrap();
assert_eq!(total, ((value - spend_value).unwrap() + value3).unwrap());
}
}
Loading