Merge pull request #1255 from zcash/orchard-scanning

zcash_client_sqlite: Implement Orchard scanning
This commit is contained in:
str4d 2024-03-11 18:54:20 +00:00 committed by GitHub
commit a9aabb2aa0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 443 additions and 59 deletions

View File

@ -427,7 +427,7 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
/// 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,

View File

@ -416,10 +416,9 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
_query: NullifierQuery,
query: NullifierQuery,
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error> {
// FIXME! Orchard.
Ok(vec![])
wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query)
}
#[cfg(feature = "transparent-inputs")]
@ -513,17 +512,29 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
&mut self,
blocks: Vec<ScannedBlock<Self::AccountId>>,
) -> 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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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::<Vec<_>>(),
)?;
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
// 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;
{
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::<Vec<_>>();
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::<Vec<_>>();
// 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)?;
}
// 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::<Vec<_>>();
// 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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wdb.conn.0,
&wdb.params,
Range {
start: start_height,
start: start_positions.height,
end: last_scanned_height + 1,
},
&note_positions,
@ -713,7 +809,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}
#[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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
)?;
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}
}
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
// 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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
)?;
}
#[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,
)?;
}
_ => (),
}

View File

@ -380,7 +380,7 @@ pub(crate) fn add_account<P: consensus::Parameters>(
// 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<P: consensus::Parameters>(
)?;
}
#[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<u32> = None;
#[cfg(not(feature = "orchard"))]
let orchard_action_count: Option<u32> = 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(())
}

View File

@ -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() {

View File

@ -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<Position>;
fn recipient_key_scope(&self) -> Option<Scope>;
}
impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
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<Position> {
Some(WalletOrchardOutput::note_commitment_tree_position(self))
}
fn recipient_key_scope(&self) -> Option<Scope> {
self.recipient_key_scope()
}
}
impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
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<Position> {
None
}
fn recipient_key_scope(&self) -> Option<Scope> {
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<T: ReceivedOrchardOutput>(
conn: &Connection,
output: &T,
tx_ref: i64,
spent_in: Option<i64>,
) -> 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<Vec<(AccountId, orchard::note::Nullifier)>, 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::<Result<_, _>>()?;
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<bool, SqliteClientError> {
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};