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`
|
||||
- `ORCHARD_SHARD_HEIGHT`
|
||||
- `BlockMetadata::orchard_tree_size`
|
||||
- `WalletSummary::next_orchard_subtree_index`
|
||||
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
|
||||
- `zcash_client_backend::fees`:
|
||||
- `orchard`
|
||||
|
@ -64,6 +65,8 @@ and this library adheres to Rust's notion of
|
|||
- `fn put_orchard_subtree_roots`
|
||||
- Added method `WalletRead::validate_seed`
|
||||
- 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`:
|
||||
- Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new`
|
||||
and the newly provided accessors instead.
|
||||
|
|
|
@ -353,6 +353,8 @@ pub struct WalletSummary<AccountId: Eq + Hash> {
|
|||
fully_scanned_height: BlockHeight,
|
||||
scan_progress: Option<Ratio<u64>>,
|
||||
next_sapling_subtree_index: u64,
|
||||
#[cfg(feature = "orchard")]
|
||||
next_orchard_subtree_index: u64,
|
||||
}
|
||||
|
||||
impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
||||
|
@ -362,14 +364,17 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
|||
chain_tip_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
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 {
|
||||
account_balances,
|
||||
chain_tip_height,
|
||||
fully_scanned_height,
|
||||
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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn is_synced(&self) -> bool {
|
||||
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:
|
||||
- `WalletMigrationError::AddressGeneration`
|
||||
- `WalletMigrationError::CannotRevert`
|
||||
- The `v_transactions` and `v_tx_outputs` views now include Orchard notes.
|
||||
|
||||
## [0.9.1] - 2024-03-09
|
||||
|
||||
|
|
|
@ -956,7 +956,7 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
|
|||
>;
|
||||
|
||||
#[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
|
||||
for<'a> F: FnMut(
|
||||
&'a mut ShardTree<
|
||||
|
@ -967,16 +967,41 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
|
|||
) -> Result<A, E>,
|
||||
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")]
|
||||
fn put_orchard_subtree_roots(
|
||||
&mut self,
|
||||
_start_index: u64,
|
||||
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
start_index: u64,
|
||||
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
) -> 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")]
|
||||
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
|
||||
for<'a> F: FnMut(
|
||||
&'a mut ShardTree<
|
||||
|
@ -1038,16 +1063,28 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
|
|||
) -> Result<A, E>,
|
||||
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")]
|
||||
fn put_orchard_subtree_roots(
|
||||
&mut self,
|
||||
_start_index: u64,
|
||||
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
start_index: u64,
|
||||
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
) -> 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>(
|
||||
&self,
|
||||
ctx: &mut CompactTx,
|
||||
nf: Self::Nullifier,
|
||||
revealed_spent_note_nullifier: Self::Nullifier,
|
||||
rng: &mut R,
|
||||
) {
|
||||
// Generate a dummy recipient.
|
||||
|
@ -977,7 +977,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
|||
};
|
||||
|
||||
let (cact, _) = compact_orchard_action(
|
||||
nf,
|
||||
revealed_spent_note_nullifier,
|
||||
recipient,
|
||||
NonNegativeAmount::ZERO,
|
||||
self.orchard_ovk(zip32::Scope::Internal),
|
||||
|
@ -997,7 +997,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
|||
mut rng: &mut R,
|
||||
) -> Self::Nullifier {
|
||||
// Generate a dummy nullifier
|
||||
let nullifier =
|
||||
let revealed_spent_note_nullifier =
|
||||
orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
|
||||
.unwrap();
|
||||
|
||||
|
@ -1008,7 +1008,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
|||
};
|
||||
|
||||
let (cact, note) = compact_orchard_action(
|
||||
nullifier,
|
||||
revealed_spent_note_nullifier,
|
||||
self.address_at(j, scope),
|
||||
value,
|
||||
self.orchard_ovk(scope),
|
||||
|
@ -1025,7 +1025,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
|||
ctx: &mut CompactTx,
|
||||
_: &P,
|
||||
_: BlockHeight,
|
||||
nf: Self::Nullifier,
|
||||
revealed_spent_note_nullifier: Self::Nullifier,
|
||||
req: AddressType,
|
||||
value: NonNegativeAmount,
|
||||
_: u32,
|
||||
|
@ -1038,7 +1038,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
|||
};
|
||||
|
||||
let (cact, note) = compact_orchard_action(
|
||||
nf,
|
||||
revealed_spent_note_nullifier,
|
||||
self.address_at(j, scope),
|
||||
value,
|
||||
self.orchard_ovk(scope),
|
||||
|
@ -1046,6 +1046,7 @@ impl TestFvk for orchard::keys::FullViewingKey {
|
|||
);
|
||||
ctx.actions.push(cact);
|
||||
|
||||
// Return the nullifier of the newly created output note
|
||||
note.nullifier(self)
|
||||
}
|
||||
}
|
||||
|
@ -1100,8 +1101,6 @@ fn compact_orchard_action<R: RngCore + CryptoRng>(
|
|||
ovk: Option<orchard::keys::OutgoingViewingKey>,
|
||||
rng: &mut R,
|
||||
) -> (CompactOrchardAction, orchard::Note) {
|
||||
let nf = nullifier.to_bytes().to_vec();
|
||||
|
||||
let rseed = {
|
||||
loop {
|
||||
let mut bytes = [0; 32];
|
||||
|
@ -1120,16 +1119,14 @@ fn compact_orchard_action<R: RngCore + CryptoRng>(
|
|||
)
|
||||
.unwrap();
|
||||
let encryptor = OrchardNoteEncryption::new(ovk, note, *MemoBytes::empty().as_array());
|
||||
let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment())
|
||||
.to_bytes()
|
||||
.to_vec();
|
||||
let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment());
|
||||
let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk()).0.to_vec();
|
||||
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
||||
|
||||
(
|
||||
CompactOrchardAction {
|
||||
nullifier: nf,
|
||||
cmx,
|
||||
nullifier: nullifier.to_bytes().to_vec(),
|
||||
cmx: cmx.to_bytes().to_vec(),
|
||||
ephemeral_key,
|
||||
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};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
crate::UtxoId,
|
||||
|
@ -948,6 +951,9 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
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 shard_store =
|
||||
SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection(
|
||||
|
@ -967,12 +973,34 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
.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(
|
||||
account_balances,
|
||||
chain_tip_height,
|
||||
fully_scanned_height,
|
||||
sapling_scan_progress,
|
||||
next_sapling_subtree_index,
|
||||
#[cfg(feature = "orchard")]
|
||||
next_orchard_subtree_index,
|
||||
);
|
||||
|
||||
Ok(Some(summary))
|
||||
|
|
|
@ -11,9 +11,8 @@ use uuid::Uuid;
|
|||
use zcash_client_backend::keys::AddressGenerationError;
|
||||
use zcash_primitives::{consensus, transaction::components::amount::BalanceError};
|
||||
|
||||
use crate::WalletDb;
|
||||
|
||||
use super::commitment_tree;
|
||||
use crate::WalletDb;
|
||||
|
||||
mod migrations;
|
||||
|
||||
|
@ -263,6 +262,51 @@ mod tests {
|
|||
ON UPDATE RESTRICT,
|
||||
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" (
|
||||
id INTEGER PRIMARY KEY,
|
||||
tx INTEGER NOT NULL,
|
||||
|
@ -390,6 +434,103 @@ mod tests {
|
|||
}
|
||||
|
||||
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
|
||||
format!(
|
||||
"CREATE VIEW v_sapling_shard_scan_ranges AS
|
||||
|
@ -456,162 +597,159 @@ mod tests {
|
|||
contains_marked".to_owned(),
|
||||
// v_transactions
|
||||
"CREATE VIEW v_transactions AS
|
||||
WITH
|
||||
notes AS (
|
||||
SELECT sapling_received_notes.id AS id,
|
||||
sapling_received_notes.account_id AS account_id,
|
||||
transactions.block AS block,
|
||||
transactions.txid AS txid,
|
||||
2 AS pool,
|
||||
sapling_received_notes.value AS value,
|
||||
CASE
|
||||
WHEN sapling_received_notes.is_change THEN 1
|
||||
ELSE 0
|
||||
END AS is_change,
|
||||
CASE
|
||||
WHEN sapling_received_notes.is_change THEN 0
|
||||
ELSE 1
|
||||
END AS received_count,
|
||||
CASE
|
||||
WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6')
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END AS memo_present
|
||||
FROM sapling_received_notes
|
||||
JOIN transactions
|
||||
ON transactions.id_tx = sapling_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,
|
||||
0 AS pool,
|
||||
utxos.value_zat AS value,
|
||||
0 AS is_change,
|
||||
1 AS received_count,
|
||||
0 AS memo_present
|
||||
FROM utxos
|
||||
UNION
|
||||
SELECT sapling_received_notes.id AS id,
|
||||
sapling_received_notes.account_id AS account_id,
|
||||
transactions.block AS block,
|
||||
transactions.txid AS txid,
|
||||
2 AS pool,
|
||||
-sapling_received_notes.value AS value,
|
||||
0 AS is_change,
|
||||
0 AS received_count,
|
||||
0 AS memo_present
|
||||
FROM sapling_received_notes
|
||||
JOIN transactions
|
||||
ON transactions.id_tx = sapling_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,
|
||||
0 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 sapling_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 sapling_received_notes
|
||||
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
||||
WHERE COALESCE(sapling_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".to_owned(),
|
||||
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,
|
||||
0 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,
|
||||
0 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".to_owned(),
|
||||
// v_tx_outputs
|
||||
"CREATE VIEW v_tx_outputs AS
|
||||
SELECT transactions.txid AS txid,
|
||||
2 AS output_pool,
|
||||
sapling_received_notes.output_index AS output_index,
|
||||
sent_notes.from_account_id AS from_account_id,
|
||||
sapling_received_notes.account_id AS to_account_id,
|
||||
NULL AS to_address,
|
||||
sapling_received_notes.value AS value,
|
||||
sapling_received_notes.is_change AS is_change,
|
||||
sapling_received_notes.memo AS memo
|
||||
FROM sapling_received_notes
|
||||
JOIN transactions
|
||||
ON transactions.id_tx = sapling_received_notes.tx
|
||||
LEFT JOIN sent_notes
|
||||
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||
(sapling_received_notes.tx, 2, sent_notes.output_index)
|
||||
UNION
|
||||
SELECT utxos.prevout_txid AS txid,
|
||||
0 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,
|
||||
sapling_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 sapling_received_notes
|
||||
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
||||
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
||||
WHERE COALESCE(sapling_received_notes.is_change, 0) = 0".to_owned(),
|
||||
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,
|
||||
0 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".to_owned(),
|
||||
];
|
||||
|
||||
let mut views_query = st
|
||||
|
|
|
@ -5,6 +5,8 @@ mod addresses_table;
|
|||
mod full_account_ids;
|
||||
mod initial_setup;
|
||||
mod nullifier_map;
|
||||
mod orchard_received_notes;
|
||||
mod orchard_shardtree;
|
||||
mod received_notes_nullable_nf;
|
||||
mod receiving_key_scopes;
|
||||
mod sapling_memo_consistency;
|
||||
|
@ -24,7 +26,7 @@ use std::rc::Rc;
|
|||
|
||||
use schemer_rusqlite::RusqliteMigration;
|
||||
use secrecy::SecretVec;
|
||||
use zcash_primitives::consensus;
|
||||
use zcash_protocol::consensus;
|
||||
|
||||
use super::WalletMigrationError;
|
||||
|
||||
|
@ -45,20 +47,22 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
|||
// |
|
||||
// v_transactions_net
|
||||
// |
|
||||
// received_notes_nullable_nf
|
||||
// / | \
|
||||
// / | \
|
||||
// shardtree_support sapling_memo_consistency nullifier_map
|
||||
// / \ \
|
||||
// add_account_birthdays receiving_key_scopes v_transactions_transparent_history
|
||||
// | \ | |
|
||||
// v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false
|
||||
// | \ | |
|
||||
// wallet_summaries \ | v_transactions_shielding_balance
|
||||
// \ | |
|
||||
// \ | v_transactions_note_uniqueness
|
||||
// \ | /
|
||||
// full_account_ids
|
||||
// received_notes_nullable_nf------
|
||||
// / | \
|
||||
// / | \
|
||||
// --------------- shardtree_support sapling_memo_consistency nullifier_map
|
||||
// / / \ \
|
||||
// orchard_shardtree add_account_birthdays receiving_key_scopes v_transactions_transparent_history
|
||||
// | \ | |
|
||||
// v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false
|
||||
// | \ | |
|
||||
// wallet_summaries \ | v_transactions_shielding_balance
|
||||
// \ | |
|
||||
// \ | v_transactions_note_uniqueness
|
||||
// \ | /
|
||||
// full_account_ids
|
||||
// |
|
||||
// orchard_received_notes
|
||||
vec![
|
||||
Box::new(initial_setup::Migration {}),
|
||||
Box::new(utxos_table::Migration {}),
|
||||
|
@ -101,5 +105,9 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
|||
seed,
|
||||
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 {
|
||||
todo!()
|
||||
s.next_orchard_subtree_index()
|
||||
}
|
||||
|
||||
fn select_spendable_notes<Cache>(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use incrementalmerkletree::{Address, Position};
|
||||
use rusqlite::{self, named_params, types::Value, OptionalExtension};
|
||||
use shardtree::error::ShardTreeError;
|
||||
use std::cmp::{max, min};
|
||||
|
@ -6,14 +7,14 @@ use std::ops::Range;
|
|||
use std::rc::Rc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use incrementalmerkletree::{Address, Position};
|
||||
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
|
||||
|
||||
use zcash_client_backend::data_api::{
|
||||
scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange},
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
use zcash_client_backend::{
|
||||
data_api::{
|
||||
scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange},
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
use zcash_protocol::{PoolType, ShieldedProtocol};
|
||||
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
|
||||
|
||||
use crate::{
|
||||
error::SqliteClientError,
|
||||
|
@ -23,6 +24,12 @@ use crate::{
|
|||
|
||||
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 {
|
||||
use ScanPriority::*;
|
||||
match priority {
|
||||
|
@ -301,6 +308,7 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
|||
wallet_note_positions: &[(ShieldedProtocol, Position)],
|
||||
) -> Result<(), SqliteClientError> {
|
||||
// Read the wallet birthday (if known).
|
||||
// TODO: use per-pool birthdays?
|
||||
let wallet_birthday = wallet_birthday(conn)?;
|
||||
|
||||
// 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.
|
||||
// We will query by subtree index to find these bounds.
|
||||
let mut required_sapling_subtrees = BTreeSet::new();
|
||||
#[cfg(feature = "orchard")]
|
||||
let mut required_orchard_subtrees = BTreeSet::new();
|
||||
for (protocol, position) in wallet_note_positions {
|
||||
match protocol {
|
||||
ShieldedProtocol::Sapling => {
|
||||
|
@ -318,6 +328,12 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
|||
);
|
||||
}
|
||||
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(
|
||||
*protocol,
|
||||
)));
|
||||
|
@ -325,14 +341,28 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
|||
}
|
||||
}
|
||||
|
||||
extend_range(
|
||||
let extended_range = extend_range(
|
||||
conn,
|
||||
&range,
|
||||
required_sapling_subtrees,
|
||||
SAPLING_TABLES_PREFIX,
|
||||
params.activation_height(NetworkUpgrade::Sapling),
|
||||
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());
|
||||
|
@ -415,9 +445,21 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
|
|||
// `ScanRange` uses an exclusive upper bound.
|
||||
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)?;
|
||||
#[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;
|
||||
|
||||
// Create a scanning range for the fragment of the last shard leading up to new tip.
|
||||
|
|
Loading…
Reference in New Issue