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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Adds a `note_version` column to the `orchard_received_notes` table.
//!
//! The Orchard protocol revision introduced by NU6.3 ([ZIP 2005]) versions Orchard note
//! plaintexts: pre-NU6.3 note plaintexts are version 2, and Ironwood note plaintexts are version
//! 3. The note plaintext version determines how the note commitment trapdoor is derived from the
//! note's `rseed`, so the version observed when a note was decrypted must be persisted in order
//! to reconstruct the note. Every existing row was decrypted under the Orchard note encryption
//! domain, which accepts only version 2 note plaintexts, so existing rows are backfilled as
//! version 2.
//!
//! [ZIP 2005]: https://zips.z.cash/zip-2005
use std::collections::HashSet;
use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use crate::wallet::init::WalletMigrationError;
use super::witness_stabilized_notes;
/// Adds a `note_version` column to the `orchard_received_notes` table.
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0x2aa44e8e_e8a7_4760_8de4_501956c969ac);
const DEPENDENCIES: &[Uuid] = &[witness_stabilized_notes::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 note_version column to the orchard_received_notes table."
}
}
impl RusqliteMigration for Migration {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
transaction.execute_batch(
"ALTER TABLE orchard_received_notes
ADD COLUMN note_version INTEGER NOT NULL DEFAULT 2;",
)?;
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}
#[cfg(test)]
mod tests {
use rusqlite::{Connection, named_params};
use schemerz_rusqlite::RusqliteMigration;
use super::Migration;
use crate::wallet::init::migrations::tests::test_migrate;
#[test]
fn migrate() {
test_migrate(&[super::MIGRATION_ID]);
}
/// The `orchard_received_notes` schema exactly as left by `account_delete_cascade` (which
/// recreated it with cascading foreign keys) and `witness_stabilized_notes` (which added
/// the `witness_stabilized` column). This state does not exist as a single `CREATE TABLE`
/// in any one migration, so it is spelled out here. Foreign-key enforcement stays off,
/// matching the real migrator, which runs migrations with foreign keys disabled.
const PRE_MIGRATION_SCHEMA: &str = "
PRAGMA foreign_keys = OFF;
CREATE TABLE orchard_received_notes (
id INTEGER PRIMARY KEY,
transaction_id 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,
commitment_tree_position INTEGER,
recipient_key_scope INTEGER,
address_id INTEGER,
witness_stabilized INTEGER NOT NULL DEFAULT 0,
UNIQUE (transaction_id, action_index)
);";
/// A wallet that received Orchard notes before NU6.3 must keep every one of those notes
/// after the migration, tagged as note version 2 with their payloads intact, and the
/// `(transaction_id, action_index)` uniqueness constraint must continue to reject a
/// duplicate note at the same action index regardless of its note version.
#[test]
fn backfills_pre_nu6_3_orchard_notes_as_version_2() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute_batch(PRE_MIGRATION_SCHEMA).unwrap();
let note_values: [i64; 3] = [10_000, 20_000, 30_000];
{
let mut stmt = conn
.prepare(
"INSERT INTO orchard_received_notes (
transaction_id, action_index, account_id, diversifier, value,
rho, rseed, nf, is_change
) VALUES (
1, :action_index, 1, :diversifier, :value,
:rho, :rseed, :nf, 0
)",
)
.unwrap();
for (i, value) in note_values.iter().enumerate() {
let action_index = i as i64;
stmt.execute(named_params! {
":action_index": action_index,
":diversifier": [i as u8; 11].as_slice(),
":value": value,
":rho": [i as u8; 32].as_slice(),
":rseed": [0x80 | i as u8; 32].as_slice(),
":nf": action_index.to_le_bytes().as_slice(),
})
.unwrap();
}
}
let tx = conn.transaction().unwrap();
Migration.up(&tx).unwrap();
// Every pre-existing note is still present, in order, tagged as version 2, with its
// value preserved.
let rows = tx
.prepare(
"SELECT action_index, value, note_version
FROM orchard_received_notes
ORDER BY action_index",
)
.unwrap()
.query_map([], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, i64>(2)?,
))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(rows.len(), note_values.len());
for (i, (action_index, value, note_version)) in rows.iter().enumerate() {
assert_eq!(*action_index, i as i64);
assert_eq!(*value, note_values[i]);
assert_eq!(*note_version, 2);
}
// A duplicate note at an existing action index is rejected even under a different note
// version; the uniqueness constraint intentionally does not include `note_version`,
// because Ironwood notes are not stored in this table.
let duplicate = tx.execute(
"INSERT INTO orchard_received_notes (
transaction_id, action_index, account_id, diversifier, value,
rho, rseed, nf, is_change, note_version
) VALUES (1, 0, 1, X'00', 1, X'01', X'02', X'ff', 0, 3)",
[],
);
assert!(duplicate.is_err());
}
}