Merge pull request #1254 from zcash/orchard-tables
zcash_client_sqlite: Add database tables for Orchard
This commit is contained in:
commit
078027221f
|
@ -22,6 +22,7 @@ and this library adheres to Rust's notion of
|
||||||
- `SentTransaction::new`
|
- `SentTransaction::new`
|
||||||
- `ORCHARD_SHARD_HEIGHT`
|
- `ORCHARD_SHARD_HEIGHT`
|
||||||
- `BlockMetadata::orchard_tree_size`
|
- `BlockMetadata::orchard_tree_size`
|
||||||
|
- `WalletSummary::next_orchard_subtree_index`
|
||||||
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
|
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
|
||||||
- `zcash_client_backend::fees`:
|
- `zcash_client_backend::fees`:
|
||||||
- `orchard`
|
- `orchard`
|
||||||
|
@ -64,6 +65,8 @@ and this library adheres to Rust's notion of
|
||||||
- `fn put_orchard_subtree_roots`
|
- `fn put_orchard_subtree_roots`
|
||||||
- Added method `WalletRead::validate_seed`
|
- Added method `WalletRead::validate_seed`
|
||||||
- Removed `Error::AccountNotFound` variant.
|
- Removed `Error::AccountNotFound` variant.
|
||||||
|
- `WalletSummary::new` now takes an additional `next_orchard_subtree_index`
|
||||||
|
argument when the `orchard` feature flag is enabled.
|
||||||
- `zcash_client_backend::decrypt`:
|
- `zcash_client_backend::decrypt`:
|
||||||
- Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new`
|
- Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new`
|
||||||
and the newly provided accessors instead.
|
and the newly provided accessors instead.
|
||||||
|
|
|
@ -353,6 +353,8 @@ pub struct WalletSummary<AccountId: Eq + Hash> {
|
||||||
fully_scanned_height: BlockHeight,
|
fully_scanned_height: BlockHeight,
|
||||||
scan_progress: Option<Ratio<u64>>,
|
scan_progress: Option<Ratio<u64>>,
|
||||||
next_sapling_subtree_index: u64,
|
next_sapling_subtree_index: u64,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
next_orchard_subtree_index: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
||||||
|
@ -362,14 +364,17 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
||||||
chain_tip_height: BlockHeight,
|
chain_tip_height: BlockHeight,
|
||||||
fully_scanned_height: BlockHeight,
|
fully_scanned_height: BlockHeight,
|
||||||
scan_progress: Option<Ratio<u64>>,
|
scan_progress: Option<Ratio<u64>>,
|
||||||
next_sapling_subtree_idx: u64,
|
next_sapling_subtree_index: u64,
|
||||||
|
#[cfg(feature = "orchard")] next_orchard_subtree_index: u64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
account_balances,
|
account_balances,
|
||||||
chain_tip_height,
|
chain_tip_height,
|
||||||
fully_scanned_height,
|
fully_scanned_height,
|
||||||
scan_progress,
|
scan_progress,
|
||||||
next_sapling_subtree_index: next_sapling_subtree_idx,
|
next_sapling_subtree_index,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
next_orchard_subtree_index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,6 +410,13 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
||||||
self.next_sapling_subtree_index
|
self.next_sapling_subtree_index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the Orchard subtree index that should start the next range of subtree
|
||||||
|
/// roots passed to [`WalletCommitmentTrees::put_orchard_subtree_roots`].
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
pub fn next_orchard_subtree_index(&self) -> u64 {
|
||||||
|
self.next_orchard_subtree_index
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns whether or not wallet scanning is complete.
|
/// Returns whether or not wallet scanning is complete.
|
||||||
pub fn is_synced(&self) -> bool {
|
pub fn is_synced(&self) -> bool {
|
||||||
self.chain_tip_height == self.fully_scanned_height
|
self.chain_tip_height == self.fully_scanned_height
|
||||||
|
|
|
@ -35,6 +35,7 @@ and this library adheres to Rust's notion of
|
||||||
- `init::WalletMigrationError` has added variants:
|
- `init::WalletMigrationError` has added variants:
|
||||||
- `WalletMigrationError::AddressGeneration`
|
- `WalletMigrationError::AddressGeneration`
|
||||||
- `WalletMigrationError::CannotRevert`
|
- `WalletMigrationError::CannotRevert`
|
||||||
|
- The `v_transactions` and `v_tx_outputs` views now include Orchard notes.
|
||||||
|
|
||||||
## [0.9.1] - 2024-03-09
|
## [0.9.1] - 2024-03-09
|
||||||
|
|
||||||
|
|
|
@ -956,7 +956,7 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
|
||||||
>;
|
>;
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
fn with_orchard_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<A, E>
|
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
|
||||||
where
|
where
|
||||||
for<'a> F: FnMut(
|
for<'a> F: FnMut(
|
||||||
&'a mut ShardTree<
|
&'a mut ShardTree<
|
||||||
|
@ -967,16 +967,41 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
|
||||||
) -> Result<A, E>,
|
) -> Result<A, E>,
|
||||||
E: From<ShardTreeError<Self::Error>>,
|
E: From<ShardTreeError<Self::Error>>,
|
||||||
{
|
{
|
||||||
todo!()
|
let tx = self
|
||||||
|
.conn
|
||||||
|
.transaction()
|
||||||
|
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
|
||||||
|
let shard_store = SqliteShardStore::from_connection(&tx, ORCHARD_TABLES_PREFIX)
|
||||||
|
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
|
||||||
|
let result = {
|
||||||
|
let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap());
|
||||||
|
callback(&mut shardtree)?
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.commit()
|
||||||
|
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
fn put_orchard_subtree_roots(
|
fn put_orchard_subtree_roots(
|
||||||
&mut self,
|
&mut self,
|
||||||
_start_index: u64,
|
start_index: u64,
|
||||||
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||||
) -> Result<(), ShardTreeError<Self::Error>> {
|
) -> Result<(), ShardTreeError<Self::Error>> {
|
||||||
todo!()
|
let tx = self
|
||||||
|
.conn
|
||||||
|
.transaction()
|
||||||
|
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
|
||||||
|
put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
|
||||||
|
&tx,
|
||||||
|
ORCHARD_TABLES_PREFIX,
|
||||||
|
start_index,
|
||||||
|
roots,
|
||||||
|
)?;
|
||||||
|
tx.commit()
|
||||||
|
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1027,7 +1052,7 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
|
||||||
>;
|
>;
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
fn with_orchard_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<A, E>
|
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
|
||||||
where
|
where
|
||||||
for<'a> F: FnMut(
|
for<'a> F: FnMut(
|
||||||
&'a mut ShardTree<
|
&'a mut ShardTree<
|
||||||
|
@ -1038,16 +1063,28 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
|
||||||
) -> Result<A, E>,
|
) -> Result<A, E>,
|
||||||
E: From<ShardTreeError<Self::Error>>,
|
E: From<ShardTreeError<Self::Error>>,
|
||||||
{
|
{
|
||||||
todo!()
|
let mut shardtree = ShardTree::new(
|
||||||
|
SqliteShardStore::from_connection(self.conn.0, ORCHARD_TABLES_PREFIX)
|
||||||
|
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
|
||||||
|
PRUNING_DEPTH.try_into().unwrap(),
|
||||||
|
);
|
||||||
|
let result = callback(&mut shardtree)?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
fn put_orchard_subtree_roots(
|
fn put_orchard_subtree_roots(
|
||||||
&mut self,
|
&mut self,
|
||||||
_start_index: u64,
|
start_index: u64,
|
||||||
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||||
) -> Result<(), ShardTreeError<Self::Error>> {
|
) -> Result<(), ShardTreeError<Self::Error>> {
|
||||||
todo!()
|
put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
|
||||||
|
self.conn.0,
|
||||||
|
ORCHARD_TABLES_PREFIX,
|
||||||
|
start_index,
|
||||||
|
roots,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -962,7 +962,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
fn add_spend<R: RngCore + CryptoRng>(
|
fn add_spend<R: RngCore + CryptoRng>(
|
||||||
&self,
|
&self,
|
||||||
ctx: &mut CompactTx,
|
ctx: &mut CompactTx,
|
||||||
nf: Self::Nullifier,
|
revealed_spent_note_nullifier: Self::Nullifier,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
) {
|
) {
|
||||||
// Generate a dummy recipient.
|
// Generate a dummy recipient.
|
||||||
|
@ -977,7 +977,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (cact, _) = compact_orchard_action(
|
let (cact, _) = compact_orchard_action(
|
||||||
nf,
|
revealed_spent_note_nullifier,
|
||||||
recipient,
|
recipient,
|
||||||
NonNegativeAmount::ZERO,
|
NonNegativeAmount::ZERO,
|
||||||
self.orchard_ovk(zip32::Scope::Internal),
|
self.orchard_ovk(zip32::Scope::Internal),
|
||||||
|
@ -997,7 +997,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
mut rng: &mut R,
|
mut rng: &mut R,
|
||||||
) -> Self::Nullifier {
|
) -> Self::Nullifier {
|
||||||
// Generate a dummy nullifier
|
// Generate a dummy nullifier
|
||||||
let nullifier =
|
let revealed_spent_note_nullifier =
|
||||||
orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
|
orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1008,7 +1008,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (cact, note) = compact_orchard_action(
|
let (cact, note) = compact_orchard_action(
|
||||||
nullifier,
|
revealed_spent_note_nullifier,
|
||||||
self.address_at(j, scope),
|
self.address_at(j, scope),
|
||||||
value,
|
value,
|
||||||
self.orchard_ovk(scope),
|
self.orchard_ovk(scope),
|
||||||
|
@ -1025,7 +1025,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
ctx: &mut CompactTx,
|
ctx: &mut CompactTx,
|
||||||
_: &P,
|
_: &P,
|
||||||
_: BlockHeight,
|
_: BlockHeight,
|
||||||
nf: Self::Nullifier,
|
revealed_spent_note_nullifier: Self::Nullifier,
|
||||||
req: AddressType,
|
req: AddressType,
|
||||||
value: NonNegativeAmount,
|
value: NonNegativeAmount,
|
||||||
_: u32,
|
_: u32,
|
||||||
|
@ -1038,7 +1038,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (cact, note) = compact_orchard_action(
|
let (cact, note) = compact_orchard_action(
|
||||||
nf,
|
revealed_spent_note_nullifier,
|
||||||
self.address_at(j, scope),
|
self.address_at(j, scope),
|
||||||
value,
|
value,
|
||||||
self.orchard_ovk(scope),
|
self.orchard_ovk(scope),
|
||||||
|
@ -1046,6 +1046,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
||||||
);
|
);
|
||||||
ctx.actions.push(cact);
|
ctx.actions.push(cact);
|
||||||
|
|
||||||
|
// Return the nullifier of the newly created output note
|
||||||
note.nullifier(self)
|
note.nullifier(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1100,8 +1101,6 @@ fn compact_orchard_action<R: RngCore + CryptoRng>(
|
||||||
ovk: Option<orchard::keys::OutgoingViewingKey>,
|
ovk: Option<orchard::keys::OutgoingViewingKey>,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
) -> (CompactOrchardAction, orchard::Note) {
|
) -> (CompactOrchardAction, orchard::Note) {
|
||||||
let nf = nullifier.to_bytes().to_vec();
|
|
||||||
|
|
||||||
let rseed = {
|
let rseed = {
|
||||||
loop {
|
loop {
|
||||||
let mut bytes = [0; 32];
|
let mut bytes = [0; 32];
|
||||||
|
@ -1120,16 +1119,14 @@ fn compact_orchard_action<R: RngCore + CryptoRng>(
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let encryptor = OrchardNoteEncryption::new(ovk, note, *MemoBytes::empty().as_array());
|
let encryptor = OrchardNoteEncryption::new(ovk, note, *MemoBytes::empty().as_array());
|
||||||
let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment())
|
let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment());
|
||||||
.to_bytes()
|
|
||||||
.to_vec();
|
|
||||||
let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk()).0.to_vec();
|
let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk()).0.to_vec();
|
||||||
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
||||||
|
|
||||||
(
|
(
|
||||||
CompactOrchardAction {
|
CompactOrchardAction {
|
||||||
nullifier: nf,
|
nullifier: nullifier.to_bytes().to_vec(),
|
||||||
cmx,
|
cmx: cmx.to_bytes().to_vec(),
|
||||||
ephemeral_key,
|
ephemeral_key,
|
||||||
ciphertext: enc_ciphertext.as_ref()[..52].to_vec(),
|
ciphertext: enc_ciphertext.as_ref()[..52].to_vec(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -110,6 +110,9 @@ use crate::{
|
||||||
|
|
||||||
use self::scanning::{parse_priority_code, priority_code, replace_queue_entries};
|
use self::scanning::{parse_priority_code, priority_code, replace_queue_entries};
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT};
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use {
|
use {
|
||||||
crate::UtxoId,
|
crate::UtxoId,
|
||||||
|
@ -948,6 +951,9 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
drop(transparent_trace);
|
drop(transparent_trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The approach used here for Sapling and Orchard subtree indexing was a quick hack
|
||||||
|
// that has not yet been replaced. TODO: Make less hacky.
|
||||||
|
// https://github.com/zcash/librustzcash/issues/1249
|
||||||
let next_sapling_subtree_index = {
|
let next_sapling_subtree_index = {
|
||||||
let shard_store =
|
let shard_store =
|
||||||
SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection(
|
SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection(
|
||||||
|
@ -967,12 +973,34 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let next_orchard_subtree_index = {
|
||||||
|
let shard_store = SqliteShardStore::<
|
||||||
|
_,
|
||||||
|
::orchard::tree::MerkleHashOrchard,
|
||||||
|
ORCHARD_SHARD_HEIGHT,
|
||||||
|
>::from_connection(tx, ORCHARD_TABLES_PREFIX)?;
|
||||||
|
|
||||||
|
// The last shard will be incomplete, and we want the next range to overlap with
|
||||||
|
// the last complete shard, so return the index of the second-to-last shard root.
|
||||||
|
shard_store
|
||||||
|
.get_shard_roots()
|
||||||
|
.map_err(ShardTreeError::Storage)?
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.nth(1)
|
||||||
|
.map(|addr| addr.index())
|
||||||
|
.unwrap_or(0)
|
||||||
|
};
|
||||||
|
|
||||||
let summary = WalletSummary::new(
|
let summary = WalletSummary::new(
|
||||||
account_balances,
|
account_balances,
|
||||||
chain_tip_height,
|
chain_tip_height,
|
||||||
fully_scanned_height,
|
fully_scanned_height,
|
||||||
sapling_scan_progress,
|
sapling_scan_progress,
|
||||||
next_sapling_subtree_index,
|
next_sapling_subtree_index,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
next_orchard_subtree_index,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Some(summary))
|
Ok(Some(summary))
|
||||||
|
|
|
@ -11,9 +11,8 @@ use uuid::Uuid;
|
||||||
use zcash_client_backend::keys::AddressGenerationError;
|
use zcash_client_backend::keys::AddressGenerationError;
|
||||||
use zcash_primitives::{consensus, transaction::components::amount::BalanceError};
|
use zcash_primitives::{consensus, transaction::components::amount::BalanceError};
|
||||||
|
|
||||||
use crate::WalletDb;
|
|
||||||
|
|
||||||
use super::commitment_tree;
|
use super::commitment_tree;
|
||||||
|
use crate::WalletDb;
|
||||||
|
|
||||||
mod migrations;
|
mod migrations;
|
||||||
|
|
||||||
|
@ -263,6 +262,51 @@ mod tests {
|
||||||
ON UPDATE RESTRICT,
|
ON UPDATE RESTRICT,
|
||||||
CONSTRAINT nf_uniq UNIQUE (spend_pool, nf)
|
CONSTRAINT nf_uniq UNIQUE (spend_pool, nf)
|
||||||
)",
|
)",
|
||||||
|
"CREATE TABLE orchard_received_notes (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
tx INTEGER NOT NULL,
|
||||||
|
action_index INTEGER NOT NULL,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
diversifier BLOB NOT NULL,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
rho BLOB NOT NULL,
|
||||||
|
rseed BLOB NOT NULL,
|
||||||
|
nf BLOB UNIQUE,
|
||||||
|
is_change INTEGER NOT NULL,
|
||||||
|
memo BLOB,
|
||||||
|
spent INTEGER,
|
||||||
|
commitment_tree_position INTEGER,
|
||||||
|
recipient_key_scope INTEGER,
|
||||||
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||||
|
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||||
|
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
||||||
|
CONSTRAINT tx_output UNIQUE (tx, action_index)
|
||||||
|
)",
|
||||||
|
"CREATE TABLE orchard_tree_cap (
|
||||||
|
-- cap_id exists only to be able to take advantage of `ON CONFLICT`
|
||||||
|
-- upsert functionality; the table will only ever contain one row
|
||||||
|
cap_id INTEGER PRIMARY KEY,
|
||||||
|
cap_data BLOB NOT NULL
|
||||||
|
)",
|
||||||
|
"CREATE TABLE orchard_tree_checkpoint_marks_removed (
|
||||||
|
checkpoint_id INTEGER NOT NULL,
|
||||||
|
mark_removed_position INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position)
|
||||||
|
)",
|
||||||
|
"CREATE TABLE orchard_tree_checkpoints (
|
||||||
|
checkpoint_id INTEGER PRIMARY KEY,
|
||||||
|
position INTEGER
|
||||||
|
)",
|
||||||
|
"CREATE TABLE orchard_tree_shards (
|
||||||
|
shard_index INTEGER PRIMARY KEY,
|
||||||
|
subtree_end_height INTEGER,
|
||||||
|
root_hash BLOB,
|
||||||
|
shard_data BLOB,
|
||||||
|
contains_marked INTEGER,
|
||||||
|
CONSTRAINT root_unique UNIQUE (root_hash)
|
||||||
|
)",
|
||||||
r#"CREATE TABLE "sapling_received_notes" (
|
r#"CREATE TABLE "sapling_received_notes" (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
tx INTEGER NOT NULL,
|
tx INTEGER NOT NULL,
|
||||||
|
@ -390,6 +434,103 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
let expected_views = vec![
|
let expected_views = vec![
|
||||||
|
// v_orchard_shard_scan_ranges
|
||||||
|
format!(
|
||||||
|
"CREATE VIEW v_orchard_shard_scan_ranges AS
|
||||||
|
SELECT
|
||||||
|
shard.shard_index,
|
||||||
|
shard.shard_index << 16 AS start_position,
|
||||||
|
(shard.shard_index + 1) << 16 AS end_position_exclusive,
|
||||||
|
IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height,
|
||||||
|
shard.subtree_end_height,
|
||||||
|
shard.contains_marked,
|
||||||
|
scan_queue.block_range_start,
|
||||||
|
scan_queue.block_range_end,
|
||||||
|
scan_queue.priority
|
||||||
|
FROM orchard_tree_shards shard
|
||||||
|
LEFT OUTER JOIN orchard_tree_shards prev_shard
|
||||||
|
ON shard.shard_index = prev_shard.shard_index + 1
|
||||||
|
-- Join with scan ranges that overlap with the subtree's involved blocks.
|
||||||
|
INNER JOIN scan_queue ON (
|
||||||
|
subtree_start_height < scan_queue.block_range_end AND
|
||||||
|
(
|
||||||
|
scan_queue.block_range_start <= shard.subtree_end_height OR
|
||||||
|
shard.subtree_end_height IS NULL
|
||||||
|
)
|
||||||
|
)",
|
||||||
|
u32::from(st.network().activation_height(NetworkUpgrade::Nu5).unwrap()),
|
||||||
|
),
|
||||||
|
//v_orchard_shard_unscanned_ranges
|
||||||
|
format!(
|
||||||
|
"CREATE VIEW v_orchard_shard_unscanned_ranges AS
|
||||||
|
WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts)
|
||||||
|
SELECT
|
||||||
|
shard_index,
|
||||||
|
start_position,
|
||||||
|
end_position_exclusive,
|
||||||
|
subtree_start_height,
|
||||||
|
subtree_end_height,
|
||||||
|
contains_marked,
|
||||||
|
block_range_start,
|
||||||
|
block_range_end,
|
||||||
|
priority
|
||||||
|
FROM v_orchard_shard_scan_ranges
|
||||||
|
INNER JOIN wallet_birthday
|
||||||
|
WHERE priority > {}
|
||||||
|
AND block_range_end > wallet_birthday.height",
|
||||||
|
priority_code(&ScanPriority::Scanned),
|
||||||
|
),
|
||||||
|
// v_orchard_shards_scan_state
|
||||||
|
"CREATE VIEW v_orchard_shards_scan_state AS
|
||||||
|
SELECT
|
||||||
|
shard_index,
|
||||||
|
start_position,
|
||||||
|
end_position_exclusive,
|
||||||
|
subtree_start_height,
|
||||||
|
subtree_end_height,
|
||||||
|
contains_marked,
|
||||||
|
MAX(priority) AS max_priority
|
||||||
|
FROM v_orchard_shard_scan_ranges
|
||||||
|
GROUP BY
|
||||||
|
shard_index,
|
||||||
|
start_position,
|
||||||
|
end_position_exclusive,
|
||||||
|
subtree_start_height,
|
||||||
|
subtree_end_height,
|
||||||
|
contains_marked".to_owned(),
|
||||||
|
// v_received_notes
|
||||||
|
"CREATE VIEW v_received_notes AS
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tx,
|
||||||
|
2 AS pool,
|
||||||
|
sapling_received_notes.output_index AS output_index,
|
||||||
|
account_id,
|
||||||
|
value,
|
||||||
|
is_change,
|
||||||
|
memo,
|
||||||
|
spent,
|
||||||
|
sent_notes.id AS sent_note_id
|
||||||
|
FROM sapling_received_notes
|
||||||
|
LEFT JOIN sent_notes
|
||||||
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||||
|
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tx,
|
||||||
|
3 AS pool,
|
||||||
|
orchard_received_notes.action_index AS output_index,
|
||||||
|
account_id,
|
||||||
|
value,
|
||||||
|
is_change,
|
||||||
|
memo,
|
||||||
|
spent,
|
||||||
|
sent_notes.id AS sent_note_id
|
||||||
|
FROM orchard_received_notes
|
||||||
|
LEFT JOIN sent_notes
|
||||||
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||||
|
(orchard_received_notes.tx, 3, orchard_received_notes.action_index)".to_owned(),
|
||||||
// v_sapling_shard_scan_ranges
|
// v_sapling_shard_scan_ranges
|
||||||
format!(
|
format!(
|
||||||
"CREATE VIEW v_sapling_shard_scan_ranges AS
|
"CREATE VIEW v_sapling_shard_scan_ranges AS
|
||||||
|
@ -456,162 +597,159 @@ mod tests {
|
||||||
contains_marked".to_owned(),
|
contains_marked".to_owned(),
|
||||||
// v_transactions
|
// v_transactions
|
||||||
"CREATE VIEW v_transactions AS
|
"CREATE VIEW v_transactions AS
|
||||||
WITH
|
WITH
|
||||||
notes AS (
|
notes AS (
|
||||||
SELECT sapling_received_notes.id AS id,
|
SELECT v_received_notes.id AS id,
|
||||||
sapling_received_notes.account_id AS account_id,
|
v_received_notes.account_id AS account_id,
|
||||||
transactions.block AS block,
|
transactions.block AS block,
|
||||||
transactions.txid AS txid,
|
transactions.txid AS txid,
|
||||||
2 AS pool,
|
v_received_notes.pool AS pool,
|
||||||
sapling_received_notes.value AS value,
|
v_received_notes.value AS value,
|
||||||
CASE
|
CASE
|
||||||
WHEN sapling_received_notes.is_change THEN 1
|
WHEN v_received_notes.is_change THEN 1
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS is_change,
|
END AS is_change,
|
||||||
CASE
|
CASE
|
||||||
WHEN sapling_received_notes.is_change THEN 0
|
WHEN v_received_notes.is_change THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END AS received_count,
|
END AS received_count,
|
||||||
CASE
|
CASE
|
||||||
WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6')
|
WHEN (v_received_notes.memo IS NULL OR v_received_notes.memo = X'F6')
|
||||||
THEN 0
|
THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END AS memo_present
|
END AS memo_present
|
||||||
FROM sapling_received_notes
|
FROM v_received_notes
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = sapling_received_notes.tx
|
ON transactions.id_tx = v_received_notes.tx
|
||||||
UNION
|
UNION
|
||||||
SELECT utxos.id AS id,
|
SELECT utxos.id AS id,
|
||||||
utxos.received_by_account_id AS account_id,
|
utxos.received_by_account_id AS account_id,
|
||||||
utxos.height AS block,
|
utxos.height AS block,
|
||||||
utxos.prevout_txid AS txid,
|
utxos.prevout_txid AS txid,
|
||||||
0 AS pool,
|
0 AS pool,
|
||||||
utxos.value_zat AS value,
|
utxos.value_zat AS value,
|
||||||
0 AS is_change,
|
0 AS is_change,
|
||||||
1 AS received_count,
|
1 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM utxos
|
FROM utxos
|
||||||
UNION
|
UNION
|
||||||
SELECT sapling_received_notes.id AS id,
|
SELECT v_received_notes.id AS id,
|
||||||
sapling_received_notes.account_id AS account_id,
|
v_received_notes.account_id AS account_id,
|
||||||
transactions.block AS block,
|
transactions.block AS block,
|
||||||
transactions.txid AS txid,
|
transactions.txid AS txid,
|
||||||
2 AS pool,
|
v_received_notes.pool AS pool,
|
||||||
-sapling_received_notes.value AS value,
|
-v_received_notes.value AS value,
|
||||||
0 AS is_change,
|
0 AS is_change,
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM sapling_received_notes
|
FROM v_received_notes
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = sapling_received_notes.spent
|
ON transactions.id_tx = v_received_notes.spent
|
||||||
UNION
|
UNION
|
||||||
SELECT utxos.id AS id,
|
SELECT utxos.id AS id,
|
||||||
utxos.received_by_account_id AS account_id,
|
utxos.received_by_account_id AS account_id,
|
||||||
transactions.block AS block,
|
transactions.block AS block,
|
||||||
transactions.txid AS txid,
|
transactions.txid AS txid,
|
||||||
0 AS pool,
|
0 AS pool,
|
||||||
-utxos.value_zat AS value,
|
-utxos.value_zat AS value,
|
||||||
0 AS is_change,
|
0 AS is_change,
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM utxos
|
FROM utxos
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = utxos.spent_in_tx
|
ON transactions.id_tx = utxos.spent_in_tx
|
||||||
),
|
),
|
||||||
sent_note_counts AS (
|
sent_note_counts AS (
|
||||||
SELECT sent_notes.from_account_id AS account_id,
|
SELECT sent_notes.from_account_id AS account_id,
|
||||||
transactions.txid AS txid,
|
transactions.txid AS txid,
|
||||||
COUNT(DISTINCT sent_notes.id) as sent_notes,
|
COUNT(DISTINCT sent_notes.id) as sent_notes,
|
||||||
SUM(
|
SUM(
|
||||||
CASE
|
CASE
|
||||||
WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR sapling_received_notes.tx IS NOT NULL)
|
WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR v_received_notes.tx IS NOT NULL)
|
||||||
THEN 0
|
THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END
|
END
|
||||||
) AS memo_count
|
) AS memo_count
|
||||||
FROM sent_notes
|
FROM sent_notes
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = sent_notes.tx
|
ON transactions.id_tx = sent_notes.tx
|
||||||
LEFT JOIN sapling_received_notes
|
LEFT JOIN v_received_notes
|
||||||
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
WHERE COALESCE(v_received_notes.is_change, 0) = 0
|
||||||
WHERE COALESCE(sapling_received_notes.is_change, 0) = 0
|
GROUP BY account_id, txid
|
||||||
GROUP BY account_id, txid
|
),
|
||||||
),
|
blocks_max_height AS (
|
||||||
blocks_max_height AS (
|
SELECT MAX(blocks.height) as max_height FROM blocks
|
||||||
SELECT MAX(blocks.height) as max_height FROM blocks
|
)
|
||||||
)
|
SELECT notes.account_id AS account_id,
|
||||||
SELECT notes.account_id AS account_id,
|
notes.block AS mined_height,
|
||||||
notes.block AS mined_height,
|
notes.txid AS txid,
|
||||||
notes.txid AS txid,
|
transactions.tx_index AS tx_index,
|
||||||
transactions.tx_index AS tx_index,
|
transactions.expiry_height AS expiry_height,
|
||||||
transactions.expiry_height AS expiry_height,
|
transactions.raw AS raw,
|
||||||
transactions.raw AS raw,
|
SUM(notes.value) AS account_balance_delta,
|
||||||
SUM(notes.value) AS account_balance_delta,
|
transactions.fee AS fee_paid,
|
||||||
transactions.fee AS fee_paid,
|
SUM(notes.is_change) > 0 AS has_change,
|
||||||
SUM(notes.is_change) > 0 AS has_change,
|
MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count,
|
||||||
MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count,
|
SUM(notes.received_count) AS received_note_count,
|
||||||
SUM(notes.received_count) AS received_note_count,
|
SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count,
|
||||||
SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count,
|
blocks.time AS block_time,
|
||||||
blocks.time AS block_time,
|
(
|
||||||
(
|
blocks.height IS NULL
|
||||||
blocks.height IS NULL
|
AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height
|
||||||
AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height
|
) AS expired_unmined
|
||||||
) AS expired_unmined
|
FROM notes
|
||||||
FROM notes
|
LEFT JOIN transactions
|
||||||
LEFT JOIN transactions
|
ON notes.txid = transactions.txid
|
||||||
ON notes.txid = transactions.txid
|
JOIN blocks_max_height
|
||||||
JOIN blocks_max_height
|
LEFT JOIN blocks ON blocks.height = notes.block
|
||||||
LEFT JOIN blocks ON blocks.height = notes.block
|
LEFT JOIN sent_note_counts
|
||||||
LEFT JOIN sent_note_counts
|
ON sent_note_counts.account_id = notes.account_id
|
||||||
ON sent_note_counts.account_id = notes.account_id
|
AND sent_note_counts.txid = notes.txid
|
||||||
AND sent_note_counts.txid = notes.txid
|
GROUP BY notes.account_id, notes.txid".to_owned(),
|
||||||
GROUP BY notes.account_id, notes.txid".to_owned(),
|
|
||||||
// v_tx_outputs
|
// v_tx_outputs
|
||||||
"CREATE VIEW v_tx_outputs AS
|
"CREATE VIEW v_tx_outputs AS
|
||||||
SELECT transactions.txid AS txid,
|
SELECT transactions.txid AS txid,
|
||||||
2 AS output_pool,
|
v_received_notes.pool AS output_pool,
|
||||||
sapling_received_notes.output_index AS output_index,
|
v_received_notes.output_index AS output_index,
|
||||||
sent_notes.from_account_id AS from_account_id,
|
sent_notes.from_account_id AS from_account_id,
|
||||||
sapling_received_notes.account_id AS to_account_id,
|
v_received_notes.account_id AS to_account_id,
|
||||||
NULL AS to_address,
|
NULL AS to_address,
|
||||||
sapling_received_notes.value AS value,
|
v_received_notes.value AS value,
|
||||||
sapling_received_notes.is_change AS is_change,
|
v_received_notes.is_change AS is_change,
|
||||||
sapling_received_notes.memo AS memo
|
v_received_notes.memo AS memo
|
||||||
FROM sapling_received_notes
|
FROM v_received_notes
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = sapling_received_notes.tx
|
ON transactions.id_tx = v_received_notes.tx
|
||||||
LEFT JOIN sent_notes
|
LEFT JOIN sent_notes
|
||||||
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
(sapling_received_notes.tx, 2, sent_notes.output_index)
|
UNION
|
||||||
UNION
|
SELECT utxos.prevout_txid AS txid,
|
||||||
SELECT utxos.prevout_txid AS txid,
|
0 AS output_pool,
|
||||||
0 AS output_pool,
|
utxos.prevout_idx AS output_index,
|
||||||
utxos.prevout_idx AS output_index,
|
NULL AS from_account_id,
|
||||||
NULL AS from_account_id,
|
utxos.received_by_account_id AS to_account_id,
|
||||||
utxos.received_by_account_id AS to_account_id,
|
utxos.address AS to_address,
|
||||||
utxos.address AS to_address,
|
utxos.value_zat AS value,
|
||||||
utxos.value_zat AS value,
|
0 AS is_change,
|
||||||
0 AS is_change,
|
NULL AS memo
|
||||||
NULL AS memo
|
FROM utxos
|
||||||
FROM utxos
|
UNION
|
||||||
UNION
|
SELECT transactions.txid AS txid,
|
||||||
SELECT transactions.txid AS txid,
|
sent_notes.output_pool AS output_pool,
|
||||||
sent_notes.output_pool AS output_pool,
|
sent_notes.output_index AS output_index,
|
||||||
sent_notes.output_index AS output_index,
|
sent_notes.from_account_id AS from_account_id,
|
||||||
sent_notes.from_account_id AS from_account_id,
|
v_received_notes.account_id AS to_account_id,
|
||||||
sapling_received_notes.account_id AS to_account_id,
|
sent_notes.to_address AS to_address,
|
||||||
sent_notes.to_address AS to_address,
|
sent_notes.value AS value,
|
||||||
sent_notes.value AS value,
|
0 AS is_change,
|
||||||
0 AS is_change,
|
sent_notes.memo AS memo
|
||||||
sent_notes.memo AS memo
|
FROM sent_notes
|
||||||
FROM sent_notes
|
JOIN transactions
|
||||||
JOIN transactions
|
ON transactions.id_tx = sent_notes.tx
|
||||||
ON transactions.id_tx = sent_notes.tx
|
LEFT JOIN v_received_notes
|
||||||
LEFT JOIN sapling_received_notes
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
WHERE COALESCE(v_received_notes.is_change, 0) = 0".to_owned(),
|
||||||
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
|
||||||
WHERE COALESCE(sapling_received_notes.is_change, 0) = 0".to_owned(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut views_query = st
|
let mut views_query = st
|
||||||
|
|
|
@ -5,6 +5,8 @@ mod addresses_table;
|
||||||
mod full_account_ids;
|
mod full_account_ids;
|
||||||
mod initial_setup;
|
mod initial_setup;
|
||||||
mod nullifier_map;
|
mod nullifier_map;
|
||||||
|
mod orchard_received_notes;
|
||||||
|
mod orchard_shardtree;
|
||||||
mod received_notes_nullable_nf;
|
mod received_notes_nullable_nf;
|
||||||
mod receiving_key_scopes;
|
mod receiving_key_scopes;
|
||||||
mod sapling_memo_consistency;
|
mod sapling_memo_consistency;
|
||||||
|
@ -24,7 +26,7 @@ use std::rc::Rc;
|
||||||
|
|
||||||
use schemer_rusqlite::RusqliteMigration;
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
use secrecy::SecretVec;
|
use secrecy::SecretVec;
|
||||||
use zcash_primitives::consensus;
|
use zcash_protocol::consensus;
|
||||||
|
|
||||||
use super::WalletMigrationError;
|
use super::WalletMigrationError;
|
||||||
|
|
||||||
|
@ -45,20 +47,22 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
||||||
// |
|
// |
|
||||||
// v_transactions_net
|
// v_transactions_net
|
||||||
// |
|
// |
|
||||||
// received_notes_nullable_nf
|
// received_notes_nullable_nf------
|
||||||
// / | \
|
// / | \
|
||||||
// / | \
|
// / | \
|
||||||
// shardtree_support sapling_memo_consistency nullifier_map
|
// --------------- shardtree_support sapling_memo_consistency nullifier_map
|
||||||
// / \ \
|
// / / \ \
|
||||||
// add_account_birthdays receiving_key_scopes v_transactions_transparent_history
|
// orchard_shardtree add_account_birthdays receiving_key_scopes v_transactions_transparent_history
|
||||||
// | \ | |
|
// | \ | |
|
||||||
// v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false
|
// v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false
|
||||||
// | \ | |
|
// | \ | |
|
||||||
// wallet_summaries \ | v_transactions_shielding_balance
|
// wallet_summaries \ | v_transactions_shielding_balance
|
||||||
// \ | |
|
// \ | |
|
||||||
// \ | v_transactions_note_uniqueness
|
// \ | v_transactions_note_uniqueness
|
||||||
// \ | /
|
// \ | /
|
||||||
// full_account_ids
|
// full_account_ids
|
||||||
|
// |
|
||||||
|
// orchard_received_notes
|
||||||
vec![
|
vec![
|
||||||
Box::new(initial_setup::Migration {}),
|
Box::new(initial_setup::Migration {}),
|
||||||
Box::new(utxos_table::Migration {}),
|
Box::new(utxos_table::Migration {}),
|
||||||
|
@ -101,5 +105,9 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
||||||
seed,
|
seed,
|
||||||
params: params.clone(),
|
params: params.clone(),
|
||||||
}),
|
}),
|
||||||
|
Box::new(orchard_shardtree::Migration {
|
||||||
|
params: params.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(orchard_received_notes::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
//! This migration adds tables to the wallet database that are needed to persist Orchard received
|
||||||
|
//! notes.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use zcash_client_backend::{PoolType, ShieldedProtocol};
|
||||||
|
|
||||||
|
use super::full_account_ids;
|
||||||
|
use crate::wallet::{init::WalletMigrationError, pool_code};
|
||||||
|
|
||||||
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x51d7a273_aa19_4109_9325_80e4a5545048);
|
||||||
|
|
||||||
|
pub(super) struct Migration;
|
||||||
|
|
||||||
|
impl schemer::Migration for Migration {
|
||||||
|
fn id(&self) -> Uuid {
|
||||||
|
MIGRATION_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
|
[full_account_ids::MIGRATION_ID].into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Add support for storage of Orchard received notes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RusqliteMigration for Migration {
|
||||||
|
type Error = WalletMigrationError;
|
||||||
|
|
||||||
|
fn up(&self, transaction: &rusqlite::Transaction<'_>) -> Result<(), Self::Error> {
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE TABLE orchard_received_notes (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
tx INTEGER NOT NULL,
|
||||||
|
action_index INTEGER NOT NULL,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
diversifier BLOB NOT NULL,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
rho BLOB NOT NULL,
|
||||||
|
rseed BLOB NOT NULL,
|
||||||
|
nf BLOB UNIQUE,
|
||||||
|
is_change INTEGER NOT NULL,
|
||||||
|
memo BLOB,
|
||||||
|
spent INTEGER,
|
||||||
|
commitment_tree_position INTEGER,
|
||||||
|
recipient_key_scope INTEGER,
|
||||||
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||||
|
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||||
|
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
||||||
|
CONSTRAINT tx_output UNIQUE (tx, action_index)
|
||||||
|
);
|
||||||
|
CREATE INDEX orchard_received_notes_account ON orchard_received_notes (
|
||||||
|
account_id ASC
|
||||||
|
);
|
||||||
|
CREATE INDEX orchard_received_notes_tx ON orchard_received_notes (
|
||||||
|
tx ASC
|
||||||
|
);
|
||||||
|
CREATE INDEX orchard_received_notes_spent ON orchard_received_notes (
|
||||||
|
spent ASC
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
transaction.execute_batch({
|
||||||
|
let sapling_pool_code = pool_code(PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||||
|
let orchard_pool_code = pool_code(PoolType::Shielded(ShieldedProtocol::Orchard));
|
||||||
|
&format!(
|
||||||
|
"CREATE VIEW v_received_notes AS
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tx,
|
||||||
|
{sapling_pool_code} AS pool,
|
||||||
|
sapling_received_notes.output_index AS output_index,
|
||||||
|
account_id,
|
||||||
|
value,
|
||||||
|
is_change,
|
||||||
|
memo,
|
||||||
|
spent,
|
||||||
|
sent_notes.id AS sent_note_id
|
||||||
|
FROM sapling_received_notes
|
||||||
|
LEFT JOIN sent_notes
|
||||||
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||||
|
(sapling_received_notes.tx, {sapling_pool_code}, sapling_received_notes.output_index)
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tx,
|
||||||
|
{orchard_pool_code} AS pool,
|
||||||
|
orchard_received_notes.action_index AS output_index,
|
||||||
|
account_id,
|
||||||
|
value,
|
||||||
|
is_change,
|
||||||
|
memo,
|
||||||
|
spent,
|
||||||
|
sent_notes.id AS sent_note_id
|
||||||
|
FROM orchard_received_notes
|
||||||
|
LEFT JOIN sent_notes
|
||||||
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||||
|
(orchard_received_notes.tx, {orchard_pool_code}, orchard_received_notes.action_index);"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
transaction.execute_batch({
|
||||||
|
let transparent_pool_code = pool_code(PoolType::Transparent);
|
||||||
|
&format!(
|
||||||
|
"DROP VIEW v_transactions;
|
||||||
|
CREATE VIEW v_transactions AS
|
||||||
|
WITH
|
||||||
|
notes AS (
|
||||||
|
SELECT v_received_notes.id AS id,
|
||||||
|
v_received_notes.account_id AS account_id,
|
||||||
|
transactions.block AS block,
|
||||||
|
transactions.txid AS txid,
|
||||||
|
v_received_notes.pool AS pool,
|
||||||
|
v_received_notes.value AS value,
|
||||||
|
CASE
|
||||||
|
WHEN v_received_notes.is_change THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS is_change,
|
||||||
|
CASE
|
||||||
|
WHEN v_received_notes.is_change THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END AS received_count,
|
||||||
|
CASE
|
||||||
|
WHEN (v_received_notes.memo IS NULL OR v_received_notes.memo = X'F6')
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END AS memo_present
|
||||||
|
FROM v_received_notes
|
||||||
|
JOIN transactions
|
||||||
|
ON transactions.id_tx = v_received_notes.tx
|
||||||
|
UNION
|
||||||
|
SELECT utxos.id AS id,
|
||||||
|
utxos.received_by_account_id AS account_id,
|
||||||
|
utxos.height AS block,
|
||||||
|
utxos.prevout_txid AS txid,
|
||||||
|
{transparent_pool_code} AS pool,
|
||||||
|
utxos.value_zat AS value,
|
||||||
|
0 AS is_change,
|
||||||
|
1 AS received_count,
|
||||||
|
0 AS memo_present
|
||||||
|
FROM utxos
|
||||||
|
UNION
|
||||||
|
SELECT v_received_notes.id AS id,
|
||||||
|
v_received_notes.account_id AS account_id,
|
||||||
|
transactions.block AS block,
|
||||||
|
transactions.txid AS txid,
|
||||||
|
v_received_notes.pool AS pool,
|
||||||
|
-v_received_notes.value AS value,
|
||||||
|
0 AS is_change,
|
||||||
|
0 AS received_count,
|
||||||
|
0 AS memo_present
|
||||||
|
FROM v_received_notes
|
||||||
|
JOIN transactions
|
||||||
|
ON transactions.id_tx = v_received_notes.spent
|
||||||
|
UNION
|
||||||
|
SELECT utxos.id AS id,
|
||||||
|
utxos.received_by_account_id AS account_id,
|
||||||
|
transactions.block AS block,
|
||||||
|
transactions.txid AS txid,
|
||||||
|
{transparent_pool_code} AS pool,
|
||||||
|
-utxos.value_zat AS value,
|
||||||
|
0 AS is_change,
|
||||||
|
0 AS received_count,
|
||||||
|
0 AS memo_present
|
||||||
|
FROM utxos
|
||||||
|
JOIN transactions
|
||||||
|
ON transactions.id_tx = utxos.spent_in_tx
|
||||||
|
),
|
||||||
|
sent_note_counts AS (
|
||||||
|
SELECT sent_notes.from_account_id AS account_id,
|
||||||
|
transactions.txid AS txid,
|
||||||
|
COUNT(DISTINCT sent_notes.id) as sent_notes,
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR v_received_notes.tx IS NOT NULL)
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END
|
||||||
|
) AS memo_count
|
||||||
|
FROM sent_notes
|
||||||
|
JOIN transactions
|
||||||
|
ON transactions.id_tx = sent_notes.tx
|
||||||
|
LEFT JOIN v_received_notes
|
||||||
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
|
WHERE COALESCE(v_received_notes.is_change, 0) = 0
|
||||||
|
GROUP BY account_id, txid
|
||||||
|
),
|
||||||
|
blocks_max_height AS (
|
||||||
|
SELECT MAX(blocks.height) as max_height FROM blocks
|
||||||
|
)
|
||||||
|
SELECT notes.account_id AS account_id,
|
||||||
|
notes.block AS mined_height,
|
||||||
|
notes.txid AS txid,
|
||||||
|
transactions.tx_index AS tx_index,
|
||||||
|
transactions.expiry_height AS expiry_height,
|
||||||
|
transactions.raw AS raw,
|
||||||
|
SUM(notes.value) AS account_balance_delta,
|
||||||
|
transactions.fee AS fee_paid,
|
||||||
|
SUM(notes.is_change) > 0 AS has_change,
|
||||||
|
MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count,
|
||||||
|
SUM(notes.received_count) AS received_note_count,
|
||||||
|
SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count,
|
||||||
|
blocks.time AS block_time,
|
||||||
|
(
|
||||||
|
blocks.height IS NULL
|
||||||
|
AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height
|
||||||
|
) AS expired_unmined
|
||||||
|
FROM notes
|
||||||
|
LEFT JOIN transactions
|
||||||
|
ON notes.txid = transactions.txid
|
||||||
|
JOIN blocks_max_height
|
||||||
|
LEFT JOIN blocks ON blocks.height = notes.block
|
||||||
|
LEFT JOIN sent_note_counts
|
||||||
|
ON sent_note_counts.account_id = notes.account_id
|
||||||
|
AND sent_note_counts.txid = notes.txid
|
||||||
|
GROUP BY notes.account_id, notes.txid;
|
||||||
|
|
||||||
|
DROP VIEW v_tx_outputs;
|
||||||
|
CREATE VIEW v_tx_outputs AS
|
||||||
|
SELECT transactions.txid AS txid,
|
||||||
|
v_received_notes.pool AS output_pool,
|
||||||
|
v_received_notes.output_index AS output_index,
|
||||||
|
sent_notes.from_account_id AS from_account_id,
|
||||||
|
v_received_notes.account_id AS to_account_id,
|
||||||
|
NULL AS to_address,
|
||||||
|
v_received_notes.value AS value,
|
||||||
|
v_received_notes.is_change AS is_change,
|
||||||
|
v_received_notes.memo AS memo
|
||||||
|
FROM v_received_notes
|
||||||
|
JOIN transactions
|
||||||
|
ON transactions.id_tx = v_received_notes.tx
|
||||||
|
LEFT JOIN sent_notes
|
||||||
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
|
UNION
|
||||||
|
SELECT utxos.prevout_txid AS txid,
|
||||||
|
{transparent_pool_code} AS output_pool,
|
||||||
|
utxos.prevout_idx AS output_index,
|
||||||
|
NULL AS from_account_id,
|
||||||
|
utxos.received_by_account_id AS to_account_id,
|
||||||
|
utxos.address AS to_address,
|
||||||
|
utxos.value_zat AS value,
|
||||||
|
0 AS is_change,
|
||||||
|
NULL AS memo
|
||||||
|
FROM utxos
|
||||||
|
UNION
|
||||||
|
SELECT transactions.txid AS txid,
|
||||||
|
sent_notes.output_pool AS output_pool,
|
||||||
|
sent_notes.output_index AS output_index,
|
||||||
|
sent_notes.from_account_id AS from_account_id,
|
||||||
|
v_received_notes.account_id AS to_account_id,
|
||||||
|
sent_notes.to_address AS to_address,
|
||||||
|
sent_notes.value AS value,
|
||||||
|
0 AS is_change,
|
||||||
|
sent_notes.memo AS memo
|
||||||
|
FROM sent_notes
|
||||||
|
JOIN transactions
|
||||||
|
ON transactions.id_tx = sent_notes.tx
|
||||||
|
LEFT JOIN v_received_notes
|
||||||
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
|
WHERE COALESCE(v_received_notes.is_change, 0) = 0;")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&self, _transaction: &rusqlite::Transaction<'_>) -> Result<(), Self::Error> {
|
||||||
|
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
//! This migration adds tables to the wallet database that are needed to persist Orchard note
|
||||||
|
//! commitment tree data using the `shardtree` crate.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rusqlite::{named_params, OptionalExtension};
|
||||||
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
|
use tracing::debug;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use zcash_client_backend::data_api::scanning::ScanPriority;
|
||||||
|
use zcash_protocol::consensus::{self, BlockHeight, NetworkUpgrade};
|
||||||
|
|
||||||
|
use super::shardtree_support;
|
||||||
|
use crate::wallet::{init::WalletMigrationError, scan_queue_extrema, scanning::priority_code};
|
||||||
|
|
||||||
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x3a6487f7_e068_42bb_9d12_6bb8dbe6da00);
|
||||||
|
|
||||||
|
pub(super) struct Migration<P> {
|
||||||
|
pub(super) params: P,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> schemer::Migration for Migration<P> {
|
||||||
|
fn id(&self) -> Uuid {
|
||||||
|
MIGRATION_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
|
[shardtree_support::MIGRATION_ID].into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Add support for storage of Orchard note commitment tree data using the `shardtree` crate."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
|
type Error = WalletMigrationError;
|
||||||
|
|
||||||
|
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
// Add shard persistence
|
||||||
|
debug!("Creating tables for Orchard shard persistence");
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE TABLE orchard_tree_shards (
|
||||||
|
shard_index INTEGER PRIMARY KEY,
|
||||||
|
subtree_end_height INTEGER,
|
||||||
|
root_hash BLOB,
|
||||||
|
shard_data BLOB,
|
||||||
|
contains_marked INTEGER,
|
||||||
|
CONSTRAINT root_unique UNIQUE (root_hash)
|
||||||
|
);
|
||||||
|
CREATE TABLE orchard_tree_cap (
|
||||||
|
-- cap_id exists only to be able to take advantage of `ON CONFLICT`
|
||||||
|
-- upsert functionality; the table will only ever contain one row
|
||||||
|
cap_id INTEGER PRIMARY KEY,
|
||||||
|
cap_data BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Add checkpoint persistence
|
||||||
|
debug!("Creating tables for checkpoint persistence");
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE TABLE orchard_tree_checkpoints (
|
||||||
|
checkpoint_id INTEGER PRIMARY KEY,
|
||||||
|
position INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE orchard_tree_checkpoint_marks_removed (
|
||||||
|
checkpoint_id INTEGER NOT NULL,
|
||||||
|
mark_removed_position INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position)
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
transaction.execute_batch(&format!(
|
||||||
|
"CREATE VIEW v_orchard_shard_scan_ranges AS
|
||||||
|
SELECT
|
||||||
|
shard.shard_index,
|
||||||
|
shard.shard_index << {} AS start_position,
|
||||||
|
(shard.shard_index + 1) << {} AS end_position_exclusive,
|
||||||
|
IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height,
|
||||||
|
shard.subtree_end_height,
|
||||||
|
shard.contains_marked,
|
||||||
|
scan_queue.block_range_start,
|
||||||
|
scan_queue.block_range_end,
|
||||||
|
scan_queue.priority
|
||||||
|
FROM orchard_tree_shards shard
|
||||||
|
LEFT OUTER JOIN orchard_tree_shards prev_shard
|
||||||
|
ON shard.shard_index = prev_shard.shard_index + 1
|
||||||
|
-- Join with scan ranges that overlap with the subtree's involved blocks.
|
||||||
|
INNER JOIN scan_queue ON (
|
||||||
|
subtree_start_height < scan_queue.block_range_end AND
|
||||||
|
(
|
||||||
|
scan_queue.block_range_start <= shard.subtree_end_height OR
|
||||||
|
shard.subtree_end_height IS NULL
|
||||||
|
)
|
||||||
|
)",
|
||||||
|
16, // ORCHARD_SHARD_HEIGHT is only available when `feature = "orchard"` is enabled.
|
||||||
|
16, // ORCHARD_SHARD_HEIGHT is only available when `feature = "orchard"` is enabled.
|
||||||
|
u32::from(self.params.activation_height(NetworkUpgrade::Nu5).unwrap()),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
transaction.execute_batch(&format!(
|
||||||
|
"CREATE VIEW v_orchard_shard_unscanned_ranges AS
|
||||||
|
WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts)
|
||||||
|
SELECT
|
||||||
|
shard_index,
|
||||||
|
start_position,
|
||||||
|
end_position_exclusive,
|
||||||
|
subtree_start_height,
|
||||||
|
subtree_end_height,
|
||||||
|
contains_marked,
|
||||||
|
block_range_start,
|
||||||
|
block_range_end,
|
||||||
|
priority
|
||||||
|
FROM v_orchard_shard_scan_ranges
|
||||||
|
INNER JOIN wallet_birthday
|
||||||
|
WHERE priority > {}
|
||||||
|
AND block_range_end > wallet_birthday.height;",
|
||||||
|
priority_code(&ScanPriority::Scanned),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE VIEW v_orchard_shards_scan_state AS
|
||||||
|
SELECT
|
||||||
|
shard_index,
|
||||||
|
start_position,
|
||||||
|
end_position_exclusive,
|
||||||
|
subtree_start_height,
|
||||||
|
subtree_end_height,
|
||||||
|
contains_marked,
|
||||||
|
MAX(priority) AS max_priority
|
||||||
|
FROM v_orchard_shard_scan_ranges
|
||||||
|
GROUP BY
|
||||||
|
shard_index,
|
||||||
|
start_position,
|
||||||
|
end_position_exclusive,
|
||||||
|
subtree_start_height,
|
||||||
|
subtree_end_height,
|
||||||
|
contains_marked;",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Treat the current best-known chain tip height as the height to use for Orchard
|
||||||
|
// initialization, bounded below by NU5 activation.
|
||||||
|
if let Some(orchard_init_height) = scan_queue_extrema(transaction)?.and_then(|r| {
|
||||||
|
self.params
|
||||||
|
.activation_height(NetworkUpgrade::Nu5)
|
||||||
|
.map(|orchard_activation| std::cmp::max(orchard_activation, *r.end()))
|
||||||
|
}) {
|
||||||
|
// If a scan range exists that contains the Orchard init height, split it in two at the
|
||||||
|
// init height.
|
||||||
|
if let Some((start, end, range_priority)) = transaction
|
||||||
|
.query_row_and_then(
|
||||||
|
"SELECT block_range_start, block_range_end, priority
|
||||||
|
FROM scan_queue
|
||||||
|
WHERE block_range_start <= :orchard_init_height
|
||||||
|
AND block_range_end > :orchard_init_height",
|
||||||
|
named_params![":orchard_init_height": u32::from(orchard_init_height)],
|
||||||
|
|row| {
|
||||||
|
let start = BlockHeight::from(row.get::<_, u32>(0)?);
|
||||||
|
let end = BlockHeight::from(row.get::<_, u32>(1)?);
|
||||||
|
let range_priority: i64 = row.get(2)?;
|
||||||
|
Ok((start, end, range_priority))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional()?
|
||||||
|
{
|
||||||
|
transaction.execute(
|
||||||
|
"DELETE from scan_queue WHERE block_range_start = :start",
|
||||||
|
named_params![":start": u32::from(start)],
|
||||||
|
)?;
|
||||||
|
if start < orchard_init_height {
|
||||||
|
// Rewrite the start of the scan range to be exactly what it was prior to the
|
||||||
|
// change.
|
||||||
|
transaction.execute(
|
||||||
|
"INSERT INTO scan_queue (block_range_start, block_range_end, priority)
|
||||||
|
VALUES (:block_range_start, :block_range_end, :priority)",
|
||||||
|
named_params![
|
||||||
|
":block_range_start": u32::from(start),
|
||||||
|
":block_range_end": u32::from(orchard_init_height),
|
||||||
|
":priority": range_priority,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
// Rewrite the remainder of the range to have at least priority `Historic`
|
||||||
|
transaction.execute(
|
||||||
|
"INSERT INTO scan_queue (block_range_start, block_range_end, priority)
|
||||||
|
VALUES (:block_range_start, :block_range_end, :priority)",
|
||||||
|
named_params![
|
||||||
|
":block_range_start": u32::from(orchard_init_height),
|
||||||
|
":block_range_end": u32::from(end),
|
||||||
|
":priority":
|
||||||
|
std::cmp::max(range_priority, priority_code(&ScanPriority::Historic)),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
// Rewrite any scanned ranges above the end of the first Orchard
|
||||||
|
// range to have at least priority `Historic`
|
||||||
|
transaction.execute(
|
||||||
|
"UPDATE scan_queue SET priority = :historic
|
||||||
|
WHERE :block_range_start >= :orchard_initial_range_end
|
||||||
|
AND priority < :historic",
|
||||||
|
named_params![
|
||||||
|
":historic": priority_code(&ScanPriority::Historic),
|
||||||
|
":orchard_initial_range_end": u32::from(end),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,7 +99,7 @@ pub(crate) mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_subtree_index(s: &WalletSummary<crate::AccountId>) -> u64 {
|
fn next_subtree_index(s: &WalletSummary<crate::AccountId>) -> u64 {
|
||||||
todo!()
|
s.next_orchard_subtree_index()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_spendable_notes<Cache>(
|
fn select_spendable_notes<Cache>(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use incrementalmerkletree::{Address, Position};
|
||||||
use rusqlite::{self, named_params, types::Value, OptionalExtension};
|
use rusqlite::{self, named_params, types::Value, OptionalExtension};
|
||||||
use shardtree::error::ShardTreeError;
|
use shardtree::error::ShardTreeError;
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
|
@ -6,14 +7,14 @@ use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use incrementalmerkletree::{Address, Position};
|
use zcash_client_backend::{
|
||||||
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
|
data_api::{
|
||||||
|
scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange},
|
||||||
use zcash_client_backend::data_api::{
|
SAPLING_SHARD_HEIGHT,
|
||||||
scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange},
|
},
|
||||||
SAPLING_SHARD_HEIGHT,
|
ShieldedProtocol,
|
||||||
};
|
};
|
||||||
use zcash_protocol::{PoolType, ShieldedProtocol};
|
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::SqliteClientError,
|
error::SqliteClientError,
|
||||||
|
@ -23,6 +24,12 @@ use crate::{
|
||||||
|
|
||||||
use super::wallet_birthday;
|
use super::wallet_birthday;
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
use zcash_client_backend::PoolType;
|
||||||
|
|
||||||
pub(crate) fn priority_code(priority: &ScanPriority) -> i64 {
|
pub(crate) fn priority_code(priority: &ScanPriority) -> i64 {
|
||||||
use ScanPriority::*;
|
use ScanPriority::*;
|
||||||
match priority {
|
match priority {
|
||||||
|
@ -301,6 +308,7 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
||||||
wallet_note_positions: &[(ShieldedProtocol, Position)],
|
wallet_note_positions: &[(ShieldedProtocol, Position)],
|
||||||
) -> Result<(), SqliteClientError> {
|
) -> Result<(), SqliteClientError> {
|
||||||
// Read the wallet birthday (if known).
|
// Read the wallet birthday (if known).
|
||||||
|
// TODO: use per-pool birthdays?
|
||||||
let wallet_birthday = wallet_birthday(conn)?;
|
let wallet_birthday = wallet_birthday(conn)?;
|
||||||
|
|
||||||
// Determine the range of block heights for which we will be updating the scan queue.
|
// Determine the range of block heights for which we will be updating the scan queue.
|
||||||
|
@ -310,6 +318,8 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
||||||
// the note commitment tree subtrees containing the positions of the discovered notes.
|
// the note commitment tree subtrees containing the positions of the discovered notes.
|
||||||
// We will query by subtree index to find these bounds.
|
// We will query by subtree index to find these bounds.
|
||||||
let mut required_sapling_subtrees = BTreeSet::new();
|
let mut required_sapling_subtrees = BTreeSet::new();
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let mut required_orchard_subtrees = BTreeSet::new();
|
||||||
for (protocol, position) in wallet_note_positions {
|
for (protocol, position) in wallet_note_positions {
|
||||||
match protocol {
|
match protocol {
|
||||||
ShieldedProtocol::Sapling => {
|
ShieldedProtocol::Sapling => {
|
||||||
|
@ -318,6 +328,12 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ShieldedProtocol::Orchard => {
|
ShieldedProtocol::Orchard => {
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
required_orchard_subtrees.insert(
|
||||||
|
Address::above_position(ORCHARD_SHARD_HEIGHT.into(), *position).index(),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
||||||
*protocol,
|
*protocol,
|
||||||
)));
|
)));
|
||||||
|
@ -325,14 +341,28 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extend_range(
|
let extended_range = extend_range(
|
||||||
conn,
|
conn,
|
||||||
&range,
|
&range,
|
||||||
required_sapling_subtrees,
|
required_sapling_subtrees,
|
||||||
SAPLING_TABLES_PREFIX,
|
SAPLING_TABLES_PREFIX,
|
||||||
params.activation_height(NetworkUpgrade::Sapling),
|
params.activation_height(NetworkUpgrade::Sapling),
|
||||||
wallet_birthday,
|
wallet_birthday,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let extended_range = extend_range(
|
||||||
|
conn,
|
||||||
|
extended_range.as_ref().unwrap_or(&range),
|
||||||
|
required_orchard_subtrees,
|
||||||
|
ORCHARD_TABLES_PREFIX,
|
||||||
|
params.activation_height(NetworkUpgrade::Nu5),
|
||||||
|
wallet_birthday,
|
||||||
)?
|
)?
|
||||||
|
.or(extended_range);
|
||||||
|
|
||||||
|
#[allow(clippy::let_and_return)]
|
||||||
|
extended_range
|
||||||
};
|
};
|
||||||
|
|
||||||
let query_range = extended_range.clone().unwrap_or_else(|| range.clone());
|
let query_range = extended_range.clone().unwrap_or_else(|| range.clone());
|
||||||
|
@ -415,9 +445,21 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
|
||||||
// `ScanRange` uses an exclusive upper bound.
|
// `ScanRange` uses an exclusive upper bound.
|
||||||
let chain_end = new_tip + 1;
|
let chain_end = new_tip + 1;
|
||||||
|
|
||||||
// Read the maximum height from the shards table.
|
// Read the maximum height from each of the the shards tables. The minimum of the two
|
||||||
|
// gives the start of a height range that covers the last incomplete shard of both the
|
||||||
|
// Sapling and Orchard pools.
|
||||||
let sapling_shard_tip = tip_shard_end_height(conn, SAPLING_TABLES_PREFIX)?;
|
let sapling_shard_tip = tip_shard_end_height(conn, SAPLING_TABLES_PREFIX)?;
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let orchard_shard_tip = tip_shard_end_height(conn, ORCHARD_TABLES_PREFIX)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let min_shard_tip = match (sapling_shard_tip, orchard_shard_tip) {
|
||||||
|
(None, None) => None,
|
||||||
|
(None, Some(o)) => Some(o),
|
||||||
|
(Some(s), None) => Some(s),
|
||||||
|
(Some(s), Some(o)) => Some(std::cmp::min(s, o)),
|
||||||
|
};
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
let min_shard_tip = sapling_shard_tip;
|
let min_shard_tip = sapling_shard_tip;
|
||||||
|
|
||||||
// Create a scanning range for the fragment of the last shard leading up to new tip.
|
// Create a scanning range for the fragment of the last shard leading up to new tip.
|
||||||
|
|
Loading…
Reference in New Issue