Merge pull request #1255 from zcash/orchard-scanning
zcash_client_sqlite: Implement Orchard scanning
This commit is contained in:
commit
a9aabb2aa0
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
¬e_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,
|
||||
)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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};
|
||||
|
|
Loading…
Reference in New Issue