diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index b16bd7b966..5a72ea9309 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -427,7 +427,7 @@ impl WalletSummary { /// belonging to a wallet. pub trait InputSource { /// The type of errors produced by a wallet backend. - type Error; + type Error: Debug; /// Backend-specific account identifier. /// @@ -498,7 +498,7 @@ pub trait InputSource { /// be abstracted away from any particular data storage substrate. pub trait WalletRead { /// The type of errors that may be generated when querying a wallet data store. - type Error; + type Error: Debug; /// The type of the account identifier. /// @@ -1312,7 +1312,8 @@ pub trait WalletWrite: WalletRead { /// At present, this only serves the Sapling protocol, but it will be modified to /// also provide operations related to Orchard note commitment trees in the future. pub trait WalletCommitmentTrees { - type Error; + type Error: Debug; + /// The type of the backing [`ShardStore`] for the Sapling note commitment tree. type SaplingShardStore<'a>: ShardStore< H = sapling::Node, diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 6b3361769e..7b3ff6194b 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -416,10 +416,9 @@ impl, P: consensus::Parameters> WalletRead for W #[cfg(feature = "orchard")] fn get_orchard_nullifiers( &self, - _query: NullifierQuery, + query: NullifierQuery, ) -> Result, Self::Error> { - // FIXME! Orchard. - Ok(vec![]) + wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query) } #[cfg(feature = "transparent-inputs")] @@ -513,17 +512,29 @@ impl WalletWrite for WalletDb &mut self, blocks: Vec>, ) -> Result<(), Self::Error> { + struct BlockPositions { + height: BlockHeight, + sapling_start_position: Position, + #[cfg(feature = "orchard")] + orchard_start_position: Position, + } + self.transactionally(|wdb| { - let start_positions = blocks.first().map(|block| { - ( - block.height(), - Position::from( - u64::from(block.sapling().final_tree_size()) - - u64::try_from(block.sapling().commitments().len()).unwrap(), - ), - ) + let start_positions = blocks.first().map(|block| BlockPositions { + height: block.height(), + sapling_start_position: Position::from( + u64::from(block.sapling().final_tree_size()) + - u64::try_from(block.sapling().commitments().len()).unwrap(), + ), + #[cfg(feature = "orchard")] + orchard_start_position: Position::from( + u64::from(block.orchard().final_tree_size()) + - u64::try_from(block.orchard().commitments().len()).unwrap(), + ), }); let mut sapling_commitments = vec![]; + #[cfg(feature = "orchard")] + let mut orchard_commitments = vec![]; let mut last_scanned_height = None; let mut note_positions = vec![]; for block in blocks.into_iter() { @@ -542,6 +553,10 @@ impl WalletWrite for WalletDb block.block_time(), block.sapling().final_tree_size(), block.sapling().commitments().len().try_into().unwrap(), + #[cfg(feature = "orchard")] + block.orchard().final_tree_size(), + #[cfg(feature = "orchard")] + block.orchard().commitments().len().try_into().unwrap(), )?; for tx in block.transactions() { @@ -551,6 +566,10 @@ impl WalletWrite for WalletDb for spend in tx.sapling_spends() { wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_row, spend.nf())?; } + #[cfg(feature = "orchard")] + for spend in tx.orchard_spends() { + wallet::orchard::mark_orchard_note_spent(wdb.conn.0, tx_row, spend.nf())?; + } for output in tx.sapling_outputs() { // Check whether this note was spent in a later block range that @@ -569,6 +588,24 @@ impl WalletWrite for WalletDb wallet::sapling::put_received_note(wdb.conn.0, output, tx_row, spent_in)?; } + #[cfg(feature = "orchard")] + for output in tx.orchard_outputs() { + // Check whether this note was spent in a later block range that + // we previously scanned. + let spent_in = output + .nf() + .map(|nf| { + wallet::query_nullifier_map::<_, Scope>( + wdb.conn.0, + ShieldedProtocol::Orchard, + &nf.to_bytes(), + ) + }) + .transpose()? + .flatten(); + + wallet::orchard::put_received_note(wdb.conn.0, output, tx_row, spent_in)?; + } } // Insert the new nullifiers from this block into the nullifier map. @@ -578,19 +615,44 @@ impl WalletWrite for WalletDb ShieldedProtocol::Sapling, block.sapling().nullifier_map(), )?; + #[cfg(feature = "orchard")] + wallet::insert_nullifier_map( + wdb.conn.0, + block.height(), + ShieldedProtocol::Orchard, + &block + .orchard() + .nullifier_map() + .iter() + .map(|(txid, idx, nfs)| { + (*txid, *idx, nfs.iter().map(|nf| nf.to_bytes()).collect()) + }) + .collect::>(), + )?; note_positions.extend(block.transactions().iter().flat_map(|wtx| { - wtx.sapling_outputs().iter().map(|out| { + let iter = wtx.sapling_outputs().iter().map(|out| { ( ShieldedProtocol::Sapling, out.note_commitment_tree_position(), ) - }) + }); + #[cfg(feature = "orchard")] + let iter = iter.chain(wtx.orchard_outputs().iter().map(|out| { + ( + ShieldedProtocol::Orchard, + out.note_commitment_tree_position(), + ) + })); + + iter })); last_scanned_height = Some(block.height()); let block_commitments = block.into_commitments(); sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some)); + #[cfg(feature = "orchard")] + orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some)); } // Prune the nullifier map of entries we no longer need. @@ -603,36 +665,70 @@ impl WalletWrite for WalletDb // We will have a start position and a last scanned height in all cases where // `blocks` is non-empty. - if let Some(((start_height, start_position), last_scanned_height)) = + if let Some((start_positions, last_scanned_height)) = start_positions.zip(last_scanned_height) { // Create subtrees from the note commitments in parallel. const CHUNK_SIZE: usize = 1024; - let subtrees = sapling_commitments - .par_chunks_mut(CHUNK_SIZE) - .enumerate() - .filter_map(|(i, chunk)| { - let start = start_position + (i * CHUNK_SIZE) as u64; - let end = start + chunk.len() as u64; - - shardtree::LocatedTree::from_iter( - start..end, - SAPLING_SHARD_HEIGHT.into(), - chunk.iter_mut().map(|n| n.take().expect("always Some")), - ) - }) - .map(|res| (res.subtree, res.checkpoints)) - .collect::>(); - - // Update the Sapling note commitment tree with all newly read note commitments - let mut subtrees = subtrees.into_iter(); - wdb.with_sapling_tree_mut::<_, _, Self::Error>(move |sapling_tree| { - for (tree, checkpoints) in &mut subtrees { - sapling_tree.insert_tree(tree, checkpoints)?; - } + { + let sapling_subtrees = sapling_commitments + .par_chunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = + start_positions.sapling_start_position + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start..end, + SAPLING_SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + // Update the Sapling note commitment tree with all newly read note commitments + let mut sapling_subtrees = sapling_subtrees.into_iter(); + wdb.with_sapling_tree_mut::<_, _, Self::Error>(move |sapling_tree| { + for (tree, checkpoints) in &mut sapling_subtrees { + sapling_tree.insert_tree(tree, checkpoints)?; + } - Ok(()) - })?; + Ok(()) + })?; + } + + // Create subtrees from the note commitments in parallel. + #[cfg(feature = "orchard")] + { + let orchard_subtrees = orchard_commitments + .par_chunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = + start_positions.orchard_start_position + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start..end, + ORCHARD_SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + // Update the Sapling note commitment tree with all newly read note commitments + let mut orchard_subtrees = orchard_subtrees.into_iter(); + wdb.with_orchard_tree_mut::<_, _, Self::Error>(move |orchard_tree| { + for (tree, checkpoints) in &mut orchard_subtrees { + orchard_tree.insert_tree(tree, checkpoints)?; + } + + Ok(()) + })?; + } // Update now-expired transactions that didn't get mined. wallet::update_expired_notes(wdb.conn.0, last_scanned_height)?; @@ -641,7 +737,7 @@ impl WalletWrite for WalletDb wdb.conn.0, &wdb.params, Range { - start: start_height, + start: start_positions.height, end: last_scanned_height + 1, }, ¬e_positions, @@ -713,7 +809,6 @@ impl WalletWrite for WalletDb } #[cfg(feature = "orchard")] - #[allow(unused_assignments)] // Remove this when the todo!()s below are implemented. for output in d_tx.orchard_outputs() { match output.transfer_type() { TransferType::Outgoing | TransferType::WalletInternal => { @@ -746,8 +841,7 @@ impl WalletWrite for WalletDb )?; if matches!(recipient, Recipient::InternalAccount(_, _)) { - todo!(); - //wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?; + wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?; } } TransferType::Incoming => { @@ -762,8 +856,7 @@ impl WalletWrite for WalletDb } } - todo!() - //wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?; + wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?; } } } @@ -776,21 +869,31 @@ impl WalletWrite for WalletDb // If we have some transparent outputs: if d_tx.tx().transparent_bundle().iter().any(|b| !b.vout.is_empty()) { - let nullifiers = wdb.get_sapling_nullifiers(NullifierQuery::All)?; - // If the transaction contains shielded spends from our wallet, we will store z->t + // If the transaction contains spends from our wallet, we will store z->t // transactions we observe in the same way they would be stored by // create_spend_to_address. - if let Some((account_id, _)) = nullifiers.iter().find( + let sapling_from_account = wdb.get_sapling_nullifiers(NullifierQuery::All)?.into_iter().find( |(_, nf)| - d_tx.tx().sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter()) + d_tx.tx().sapling_bundle().into_iter().flat_map(|b| b.shielded_spends().iter()) .any(|input| nf == input.nullifier()) - ) { + ).map(|(account_id, _)| account_id); + + #[cfg(feature = "orchard")] + let orchard_from_account = wdb.get_orchard_nullifiers(NullifierQuery::All)?.into_iter().find( + |(_, nf)| + d_tx.tx().orchard_bundle().iter().flat_map(|b| b.actions().iter()) + .any(|input| nf == input.nullifier()) + ).map(|(account_id, _)| account_id); + #[cfg(not(feature = "orchard"))] + let orchard_from_account = None; + + if let Some(account_id) = orchard_from_account.or(sapling_from_account) { for (output_index, txout) in d_tx.tx().transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() { if let Some(address) = txout.recipient_address() { wallet::put_sent_output( wdb.conn.0, &wdb.params, - *account_id, + account_id, tx_ref, output_index, &Recipient::Transparent(address), @@ -865,8 +968,21 @@ impl WalletWrite for WalletDb )?; } #[cfg(feature = "orchard")] - Recipient::InternalAccount(_account, Note::Orchard(_note)) => { - todo!(); + Recipient::InternalAccount(account, Note::Orchard(note)) => { + wallet::orchard::put_received_note( + wdb.conn.0, + &DecryptedOutput::new( + output.output_index(), + *note, + *account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + None, + )?; } _ => (), } diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index c29eac1b13..8b51e167f4 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -380,7 +380,7 @@ pub(crate) fn add_account( // If a birthday frontier is available, insert it into the note commitment tree. If the // birthday frontier is the empty frontier, we don't need to do anything. if let Some(frontier) = birthday.sapling_frontier().value() { - debug!("Inserting frontier into ShardTree: {:?}", frontier); + debug!("Inserting Sapling frontier into ShardTree: {:?}", frontier); let shard_store = SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection( conn, @@ -405,6 +405,34 @@ pub(crate) fn add_account( )?; } + #[cfg(feature = "orchard")] + if let Some(frontier) = birthday.orchard_frontier().value() { + debug!("Inserting Orchard frontier into ShardTree: {:?}", frontier); + let shard_store = SqliteShardStore::< + _, + ::orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >::from_connection(conn, ORCHARD_TABLES_PREFIX)?; + let mut shard_tree: ShardTree< + _, + { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + ORCHARD_SHARD_HEIGHT, + > = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + shard_tree.insert_frontier_nodes( + frontier.clone(), + Retention::Checkpoint { + // This subtraction is safe, because all leaves in the tree appear in blocks, and + // the invariant that birthday.height() always corresponds to the block for which + // `frontier` is the tree state at the start of the block. Together, this means + // there exists a prior block for which frontier is the tree state at the end of + // the block. + id: birthday.height() - 1, + is_marked: false, + }, + )?; + } + + // The ignored range always starts at Sapling activation let sapling_activation_height = params .activation_height(NetworkUpgrade::Sapling) .expect("Sapling activation height must be available."); @@ -1806,6 +1834,7 @@ pub(crate) fn get_account_ids( } /// Inserts information about a scanned block into the database. +#[allow(clippy::too_many_arguments)] pub(crate) fn put_block( conn: &rusqlite::Transaction<'_>, block_height: BlockHeight, @@ -1813,6 +1842,8 @@ pub(crate) fn put_block( block_time: u32, sapling_commitment_tree_size: u32, sapling_output_count: u32, + #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32, + #[cfg(feature = "orchard")] orchard_action_count: u32, ) -> Result<(), SqliteClientError> { let block_hash_data = conn .query_row( @@ -1843,7 +1874,9 @@ pub(crate) fn put_block( time, sapling_commitment_tree_size, sapling_output_count, - sapling_tree + sapling_tree, + orchard_commitment_tree_size, + orchard_action_count ) VALUES ( :height, @@ -1851,21 +1884,32 @@ pub(crate) fn put_block( :block_time, :sapling_commitment_tree_size, :sapling_output_count, - x'00' + x'00', + :orchard_commitment_tree_size, + :orchard_action_count ) ON CONFLICT (height) DO UPDATE SET hash = :hash, time = :block_time, sapling_commitment_tree_size = :sapling_commitment_tree_size, - sapling_output_count = :sapling_output_count", + sapling_output_count = :sapling_output_count, + orchard_commitment_tree_size = :orchard_commitment_tree_size, + orchard_action_count = :orchard_action_count", )?; + #[cfg(not(feature = "orchard"))] + let orchard_commitment_tree_size: Option = None; + #[cfg(not(feature = "orchard"))] + let orchard_action_count: Option = None; + stmt_upsert_block.execute(named_params![ ":height": u32::from(block_height), ":hash": &block_hash.0[..], ":block_time": block_time, ":sapling_commitment_tree_size": sapling_commitment_tree_size, ":sapling_output_count": sapling_output_count, + ":orchard_commitment_tree_size": orchard_commitment_tree_size, + ":orchard_action_count": orchard_action_count, ])?; Ok(()) @@ -2050,13 +2094,20 @@ pub(crate) fn update_expired_notes( conn: &rusqlite::Connection, expiry_height: BlockHeight, ) -> Result<(), SqliteClientError> { - let mut stmt_update_expired = conn.prepare_cached( + let mut stmt_update_sapling_expired = conn.prepare_cached( "UPDATE sapling_received_notes SET spent = NULL WHERE EXISTS ( SELECT id_tx FROM transactions WHERE id_tx = sapling_received_notes.spent AND block IS NULL AND expiry_height < ? )", )?; - stmt_update_expired.execute([u32::from(expiry_height)])?; + stmt_update_sapling_expired.execute([u32::from(expiry_height)])?; + let mut stmt_update_orchard_expired = conn.prepare_cached( + "UPDATE orchard_received_notes SET spent = NULL WHERE EXISTS ( + SELECT id_tx FROM transactions + WHERE id_tx = orchard_received_notes.spent AND block IS NULL AND expiry_height < ? + )", + )?; + stmt_update_orchard_expired.execute([u32::from(expiry_height)])?; Ok(()) } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs index c3540bf2e4..b8006f5d99 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -652,6 +652,10 @@ mod tests { block.block_time(), block.sapling().final_tree_size(), block.sapling().commitments().len().try_into().unwrap(), + #[cfg(feature = "orchard")] + block.orchard().final_tree_size(), + #[cfg(feature = "orchard")] + block.orchard().commitments().len().try_into().unwrap(), )?; for tx in block.transactions() { diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index fac22c8e24..f6ac8a3128 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -1,3 +1,215 @@ +use incrementalmerkletree::Position; +use rusqlite::{named_params, params, Connection}; + +use zcash_client_backend::{ + data_api::NullifierQuery, wallet::WalletOrchardOutput, DecryptedOutput, TransferType, +}; +use zcash_protocol::memo::MemoBytes; +use zip32::Scope; + +use crate::{error::SqliteClientError, AccountId}; + +use super::{memo_repr, scope_code}; + +/// This trait provides a generalization over shielded output representations. +pub(crate) trait ReceivedOrchardOutput { + fn index(&self) -> usize; + fn account_id(&self) -> AccountId; + fn note(&self) -> &orchard::note::Note; + fn memo(&self) -> Option<&MemoBytes>; + fn is_change(&self) -> bool; + fn nullifier(&self) -> Option<&orchard::note::Nullifier>; + fn note_commitment_tree_position(&self) -> Option; + fn recipient_key_scope(&self) -> Option; +} + +impl ReceivedOrchardOutput for WalletOrchardOutput { + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> AccountId { + *WalletOrchardOutput::account_id(self) + } + fn note(&self) -> &orchard::note::Note { + WalletOrchardOutput::note(self) + } + fn memo(&self) -> Option<&MemoBytes> { + None + } + fn is_change(&self) -> bool { + WalletOrchardOutput::is_change(self) + } + fn nullifier(&self) -> Option<&orchard::note::Nullifier> { + self.nf() + } + fn note_commitment_tree_position(&self) -> Option { + Some(WalletOrchardOutput::note_commitment_tree_position(self)) + } + fn recipient_key_scope(&self) -> Option { + self.recipient_key_scope() + } +} + +impl ReceivedOrchardOutput for DecryptedOutput { + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> AccountId { + *self.account() + } + fn note(&self) -> &orchard::note::Note { + self.note() + } + fn memo(&self) -> Option<&MemoBytes> { + Some(self.memo()) + } + fn is_change(&self) -> bool { + self.transfer_type() == TransferType::WalletInternal + } + fn nullifier(&self) -> Option<&orchard::note::Nullifier> { + None + } + fn note_commitment_tree_position(&self) -> Option { + None + } + fn recipient_key_scope(&self) -> Option { + if self.transfer_type() == TransferType::WalletInternal { + Some(Scope::Internal) + } else { + Some(Scope::External) + } + } +} + +/// Records the specified shielded output as having been received. +/// +/// This implementation relies on the facts that: +/// - A transaction will not contain more than 2^63 shielded outputs. +/// - A note value will never exceed 2^63 zatoshis. +pub(crate) fn put_received_note( + conn: &Connection, + output: &T, + tx_ref: i64, + spent_in: Option, +) -> Result<(), SqliteClientError> { + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO orchard_received_notes + (tx, action_index, account_id, diversifier, value, rseed, memo, nf, + is_change, spent, commitment_tree_position, + recipient_key_scope) + VALUES ( + :tx, + :action_index, + :account_id, + :diversifier, + :value, + :rseed, + :memo, + :nf, + :is_change, + :spent, + :commitment_tree_position, + :recipient_key_scope + ) + ON CONFLICT (tx, action_index) DO UPDATE + SET account_id = :account_id, + diversifier = :diversifier, + value = :value, + rseed = :rseed, + nf = IFNULL(:nf, nf), + memo = IFNULL(:memo, memo), + is_change = IFNULL(:is_change, is_change), + spent = IFNULL(:spent, spent), + commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position), + recipient_key_scope = :recipient_key_scope", + )?; + + let rseed = output.note().rseed(); + let to = output.note().recipient(); + let diversifier = to.diversifier(); + + let sql_args = named_params![ + ":tx": &tx_ref, + ":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"), + ":account_id": output.account_id().0, + ":diversifier": diversifier.as_array(), + ":value": output.note().value().inner(), + ":rseed": &rseed.as_bytes(), + ":nf": output.nullifier().map(|nf| nf.to_bytes()), + ":memo": memo_repr(output.memo()), + ":is_change": output.is_change(), + ":spent": spent_in, + ":commitment_tree_position": output.note_commitment_tree_position().map(u64::from), + ":recipient_key_scope": output.recipient_key_scope().map(scope_code), + ]; + + stmt_upsert_received_note + .execute(sql_args) + .map_err(SqliteClientError::from)?; + + Ok(()) +} + +/// Retrieves the set of nullifiers for "potentially spendable" Orchard notes that the +/// wallet is tracking. +/// +/// "Potentially spendable" means: +/// - The transaction in which the note was created has been observed as mined. +/// - No transaction in which the note's nullifier appears has been observed as mined. +pub(crate) fn get_orchard_nullifiers( + conn: &Connection, + query: NullifierQuery, +) -> Result, SqliteClientError> { + // Get the nullifiers for the notes we are tracking + let mut stmt_fetch_nullifiers = match query { + NullifierQuery::Unspent => conn.prepare( + "SELECT rn.id, rn.account_id, rn.nf + FROM orchard_received_notes rn + LEFT OUTER JOIN transactions tx + ON tx.id_tx = rn.spent + WHERE tx.block IS NULL + AND nf IS NOT NULL", + )?, + NullifierQuery::All => conn.prepare( + "SELECT rn.id, rn.account_id, rn.nf + FROM orchard_received_notes rn + WHERE nf IS NOT NULL", + )?, + }; + + let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| { + let account = AccountId(row.get(1)?); + let nf_bytes: [u8; 32] = row.get(2)?; + Ok::<_, rusqlite::Error>(( + account, + orchard::note::Nullifier::from_bytes(&nf_bytes).unwrap(), + )) + })?; + + let res: Vec<_> = nullifiers.collect::>()?; + Ok(res) +} + +/// Marks a given nullifier as having been revealed in the construction +/// of the specified transaction. +/// +/// Marking a note spent in this fashion does NOT imply that the +/// spending transaction has been mined. +pub(crate) fn mark_orchard_note_spent( + conn: &Connection, + tx_ref: i64, + nf: &orchard::note::Nullifier, +) -> Result { + let mut stmt_mark_orchard_note_spent = + conn.prepare_cached("UPDATE orchard_received_notes SET spent = ? WHERE nf = ?")?; + + match stmt_mark_orchard_note_spent.execute(params![tx_ref, nf.to_bytes()])? { + 0 => Ok(false), + 1 => Ok(true), + _ => unreachable!("nf column is marked as UNIQUE"), + } +} + #[cfg(test)] pub(crate) mod tests { use incrementalmerkletree::{Hashable, Level};