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
//! Reports transparent outputs in `v_tx_outputs` at their transparent receiver address.
//!
//! The received-output arm of `v_tx_outputs` resolved every receiving address through
//! `addresses.address`, which for an external-scope entry holds the unified encoding; the
//! transparent receiver lives only in `addresses.cached_transparent_receiver_address`. Every
//! transparent output received by the wallet at such an address was therefore reported with
//! the enclosing unified address as its `to_address`, misrepresenting what was observably
//! paid on chain.
//!
//! For a payment the wallet made to one of its own transparent addresses the effect was
//! compounded: the transaction produces both a `sent_notes` row carrying the transparent
//! address that was actually paid and a received-output row carrying the unified address,
//! and the view merged the two with `MAX(to_address)` — under SQLite's BINARY collation
//! `'u' > 't'`, so the unified address deterministically shadowed the recorded recipient.
//!
//! This migration recreates `v_tx_outputs` so that the received-output arm reports the
//! transparent receiver itself for transparent outputs, and so that the merge prefers the
//! recipient address recorded when the wallet created the output over the receiving address,
//! rather than choosing between them by byte order. See zcash/librustzcash#2845.
use std::collections::HashSet;
use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use crate::wallet::init::WalletMigrationError;
use super::{ironwood_pool_code_views, v_tx_outputs_key_scopes};
/// This migration reports transparent outputs in `v_tx_outputs` at their transparent
/// receiver address, and makes the recorded sent-to address authoritative for outputs the
/// wallet created.
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0x856ecde7_c670_47c1_9345_b80ba5b12c4f);
/// `v_tx_outputs_key_scopes` is the prior owner of the `v_tx_outputs` definition this
/// migration recreates; `ironwood_pool_code_views` supplies the current `v_received_outputs`
/// definition whose pool codes the transparent-receiver case discriminates on.
const DEPENDENCIES: &[Uuid] = &[
v_tx_outputs_key_scopes::MIGRATION_ID,
ironwood_pool_code_views::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 {
"Reports transparent outputs in v_tx_outputs at their transparent receiver address."
}
}
impl RusqliteMigration for Migration {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
transaction.execute_batch(
"DROP VIEW v_tx_outputs;
CREATE VIEW v_tx_outputs AS
WITH unioned AS (
-- select all outputs received by the wallet
SELECT t.id_tx AS transaction_id,
t.txid AS txid,
t.mined_height AS mined_height,
IFNULL(t.trust_status, 0) AS trust_status,
ro.pool AS output_pool,
ro.output_index AS output_index,
from_account.uuid AS from_account_uuid,
to_account.uuid AS to_account_uuid,
-- for a transparent output, the address at which it was received is
-- the transparent receiver itself, not a unified address containing it
CASE ro.pool
WHEN 0 THEN a.cached_transparent_receiver_address
ELSE a.address
END AS to_address,
0 AS is_sent_row,
a.diversifier_index_be AS diversifier_index_be,
ro.value AS value,
ro.is_change AS is_change,
ro.memo AS memo,
a.key_scope AS recipient_key_scope
FROM v_received_outputs ro
JOIN transactions t
ON t.id_tx = ro.transaction_id
LEFT JOIN addresses a ON a.id = ro.address_id
-- join to the sent_notes table to obtain `from_account_id`
LEFT JOIN sent_notes ON sent_notes.id = ro.sent_note_id
-- join on the accounts table to obtain account UUIDs
LEFT JOIN accounts from_account ON from_account.id = sent_notes.from_account_id
LEFT JOIN accounts to_account ON to_account.id = ro.account_id
UNION ALL
-- select all outputs sent by the wallet
SELECT t.id_tx AS transaction_id,
t.txid AS txid,
t.mined_height AS mined_height,
IFNULL(t.trust_status, 0) AS trust_status,
sent_notes.output_pool AS output_pool,
sent_notes.output_index AS output_index,
from_account.uuid AS from_account_uuid,
NULL AS to_account_uuid,
sent_notes.to_address AS to_address,
1 AS is_sent_row,
NULL AS diversifier_index_be,
sent_notes.value AS value,
0 AS is_change,
sent_notes.memo AS memo,
NULL AS recipient_key_scope
FROM sent_notes
JOIN transactions t
ON t.id_tx = sent_notes.transaction_id
LEFT JOIN v_received_outputs ro ON ro.sent_note_id = sent_notes.id
-- join on the accounts table to obtain account UUIDs
LEFT JOIN accounts from_account ON from_account.id = sent_notes.from_account_id
)
-- merge duplicate rows while retaining maximum information
SELECT
transaction_id,
MAX(txid) AS txid,
MAX(mined_height) AS tx_mined_height,
MIN(trust_status) AS tx_trust_status,
output_pool,
output_index,
MAX(from_account_uuid) AS from_account_uuid,
MAX(to_account_uuid) AS to_account_uuid,
-- the recipient address recorded when the wallet created the output is
-- authoritative; the receiving address is reported only for outputs the
-- wallet did not create
COALESCE(
MAX(CASE WHEN is_sent_row THEN to_address END),
MAX(CASE WHEN NOT is_sent_row THEN to_address END)
) AS to_address,
MAX(value) AS value,
MAX(is_change) AS is_change,
MAX(memo) AS memo,
MAX(recipient_key_scope) AS recipient_key_scope
FROM unioned
GROUP BY transaction_id, output_pool, output_index;
",
)?;
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}
#[cfg(test)]
mod tests {
use crate::wallet::init::migrations::tests::test_migrate;
#[test]
fn migrate() {
test_migrate(&[super::MIGRATION_ID]);
}
}