1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//! Adds a column recording how each transaction classifies against ZIP 318.
//!
//! A wallet that wants to label a pool-migration transaction in its history cannot rely on the
//! migration plan it scheduled the transaction from: that plan does not survive a seed restore, and
//! it says nothing about a transaction the wallet did not itself schedule. The classification is
//! instead derived from the transaction's shape, which is recoverable from chain data, and stored
//! here so that it is available to the same SQL a client already uses to read its history.
//!
//! It is stored rather than computed on read because the evidence it rests on is only all present
//! at once while the transaction is being decrypted: the action counts come from the raw
//! transaction, and whether the outputs are the account's own comes from decryption. Neither is
//! available to a view.
//!
//! # The default is "not classified", not "not a migration"
//!
//! The column defaults to zero, which decodes to
//! [`Zip318Classification::Unknown`](zcash_protocol::zip318::Zip318Classification::Unknown). This
//! migration deliberately does NOT classify existing rows. Doing so would mean re-deriving the
//! classification from stored columns alone, without the decrypted outputs, which is both the
//! weakest evidence available and a second implementation of the predicate to keep in step with the
//! real one.
//!
//! So every transaction already in the wallet keeps the default, and needs the transaction
//! rescanned before it can be labelled. That is a real value rather than NULL so those rows can be
//! found with an ordinary query, and it is distinct from the code for "nonconforming", which is a
//! decision. A client MUST render the default as no label, never as "not a migration": the two mean
//! "we never looked" and "we looked and it is not one".
use std::collections::HashSet;
use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use crate::wallet::init::WalletMigrationError;
use super::v_transactions_pool_crossing;
/// This migration adds the `zip318_kind` column to the `transactions` table.
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0x0a35a9e4_6c1d_4f7a_9c02_51b8de4f7c33);
/// `v_transactions_pool_crossing` is the most recent migration to touch how transactions are
/// classified for history purposes; ordering after it keeps the two classifications' migrations in
/// a single chain rather than letting them race.
const DEPENDENCIES: &[Uuid] = &[v_transactions_pool_crossing::MIGRATION_ID];
pub(super) struct Migration;
impl schemerz::Migration<Uuid> for Migration {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
DEPENDENCIES.iter().copied().collect()
}
fn description(&self) -> &'static str {
"Adds a column recording each transaction's ZIP 318 classification."
}
}
impl RusqliteMigration for Migration {
type Error = WalletMigrationError;
fn up(&self, conn: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// Zero is the code for "not classified"; see the module documentation for why existing
// rows keep it rather than being classified here.
conn.execute_batch(
"ALTER TABLE transactions
ADD COLUMN zip318_kind INTEGER NOT NULL DEFAULT 0;",
)?;
Ok(())
}
fn down(&self, conn: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
conn.execute_batch("ALTER TABLE transactions DROP COLUMN zip318_kind;")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use zcash_protocol::zip318::Zip318Classification;
use crate::wallet::init::migrations::tests::test_migrate;
#[test]
fn migrate() {
test_migrate(&[super::MIGRATION_ID]);
}
/// The column's SQL default must be the code for "not classified", so that a row this
/// migration did not classify reads back as unclassified rather than as a decision that the
/// transaction is not a migration transaction. The DDL is asserted against the schema constant
/// by [`test_migrate`]; this pins the constant the DDL has to carry.
#[test]
fn the_column_default_means_unclassified() {
assert_eq!(Zip318Classification::Unknown.to_code(), 0);
assert_ne!(Zip318Classification::Nonconforming.to_code(), 0);
}
}