1use core::ops::Deref;
2use std_shims::{vec, vec::Vec, collections::HashMap};
3
4use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
5
6use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
7
8use monero_rpc::ScannableBlock;
9use monero_oxide::{
10 io::*,
11 primitives::Commitment,
12 transaction::{Timelock, Pruned, Transaction},
13};
14use crate::{
15 address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra,
16 SharedKeyDerivations,
17};
18
19#[derive(Zeroize, ZeroizeOnDrop)]
21pub struct Timelocked(Vec<WalletOutput>);
22
23impl Timelocked {
24 #[must_use]
26 pub fn not_additionally_locked(self) -> Vec<WalletOutput> {
27 let mut res = vec![];
28 for output in &self.0 {
29 if output.additional_timelock() == Timelock::None {
30 res.push(output.clone());
31 }
32 }
33 res
34 }
35
36 #[must_use]
48 pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
49 let mut res = vec![];
50 for output in &self.0 {
51 if (output.additional_timelock() <= Timelock::Block(block)) ||
52 (output.additional_timelock() <= Timelock::Time(time))
53 {
54 res.push(output.clone());
55 }
56 }
57 res
58 }
59
60 #[must_use]
62 pub fn ignore_additional_timelock(mut self) -> Vec<WalletOutput> {
63 let mut res = vec![];
64 core::mem::swap(&mut self.0, &mut res);
65 res
66 }
67}
68
69#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
71pub enum ScanError {
72 #[error("unsupported protocol version ({0})")]
74 UnsupportedProtocol(u8),
75 #[error("invalid scannable block ({0})")]
77 InvalidScannableBlock(&'static str),
78}
79
80#[derive(Clone)]
81struct InternalScanner {
82 pair: ViewPair,
83 guaranteed: bool,
84 subaddresses: HashMap<CompressedEdwardsY, Option<SubaddressIndex>>,
85}
86
87impl Zeroize for InternalScanner {
88 fn zeroize(&mut self) {
89 self.pair.zeroize();
90 self.guaranteed.zeroize();
91
92 for (mut key, mut value) in self.subaddresses.drain() {
94 key.zeroize();
95 value.zeroize();
96 }
97 }
98}
99impl Drop for InternalScanner {
100 fn drop(&mut self) {
101 self.zeroize();
102 }
103}
104impl ZeroizeOnDrop for InternalScanner {}
105
106impl InternalScanner {
107 fn new(pair: ViewPair, guaranteed: bool) -> Self {
108 let mut subaddresses = HashMap::new();
109 subaddresses.insert(pair.spend().compress(), None);
110 Self { pair, guaranteed, subaddresses }
111 }
112
113 fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
114 let (spend, _) = self.pair.subaddress_keys(subaddress);
115 self.subaddresses.insert(spend.compress(), Some(subaddress));
116 }
117
118 fn scan_transaction(
119 &self,
120 output_index_for_first_ringct_output: u64,
121 tx_hash: [u8; 32],
122 tx: &Transaction<Pruned>,
123 ) -> Result<Timelocked, ScanError> {
124 if tx.version() != 2 {
127 return Ok(Timelocked(vec![]));
128 }
129
130 let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) else {
132 return Ok(Timelocked(vec![]));
133 };
134
135 let Some((tx_keys, additional)) = extra.keys() else {
136 return Ok(Timelocked(vec![]));
137 };
138 let payment_id = extra.payment_id();
139
140 let mut res = vec![];
141 for (o, output) in tx.prefix().outputs.iter().enumerate() {
142 let Some(output_key) = decompress_point(output.key.to_bytes()) else { continue };
143
144 let additional = additional.as_ref().map(|additional| additional.get(o));
152
153 #[allow(clippy::manual_let_else)]
154 for key in tx_keys.iter().map(|key| Some(Some(key))).chain(core::iter::once(additional)) {
155 let key = match key {
157 Some(Some(key)) => key,
158 Some(None) | None => continue,
159 };
160 let ecdh = Zeroizing::new(self.pair.view.deref() * key);
162 let output_derivations = SharedKeyDerivations::output_derivations(
163 if self.guaranteed {
164 Some(SharedKeyDerivations::uniqueness(&tx.prefix().inputs))
165 } else {
166 None
167 },
168 ecdh.clone(),
169 o,
170 );
171
172 if let Some(actual_view_tag) = output.view_tag {
174 if actual_view_tag != output_derivations.view_tag {
175 continue;
176 }
177 }
178
179 let Some(subaddress) = ({
181 let subaddress_spend_key =
186 output_key - (&output_derivations.shared_key * ED25519_BASEPOINT_TABLE);
187 self.subaddresses.get(&subaddress_spend_key.compress())
188 }) else {
189 continue;
190 };
191 let subaddress = *subaddress;
192
193 let mut key_offset = output_derivations.shared_key;
195 if let Some(subaddress) = subaddress {
196 key_offset += self.pair.subaddress_derivation(subaddress);
199 }
200 let mut commitment = Commitment::zero();
202
203 if let Some(amount) = output.amount {
205 commitment.amount = amount;
206 } else {
208 let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
209 Err(ScanError::InvalidScannableBlock("non-miner v2 transaction without RCT proofs"))?
211 };
212
213 commitment = match proofs.base.encrypted_amounts.get(o) {
214 Some(amount) => output_derivations.decrypt(amount),
215 None => Err(ScanError::InvalidScannableBlock(
217 "RCT proofs without an encrypted amount per output",
218 ))?,
219 };
220
221 if Some(&commitment.calculate()) != proofs.base.commitments.get(o) {
223 continue;
224 }
225 }
226
227 let payment_id = payment_id.map(|id| id ^ SharedKeyDerivations::payment_id_xor(ecdh));
229
230 let o = u64::try_from(o).expect("couldn't convert output index (usize) to u64");
231
232 res.push(WalletOutput {
233 absolute_id: AbsoluteId { transaction: tx_hash, index_in_transaction: o },
234 relative_id: RelativeId {
235 index_on_blockchain: output_index_for_first_ringct_output.checked_add(o).ok_or(
236 ScanError::InvalidScannableBlock(
237 "transaction's output's index isn't representable as a u64",
238 ),
239 )?,
240 },
241 data: OutputData { key: output_key, key_offset, commitment },
242 metadata: Metadata {
243 additional_timelock: tx.prefix().additional_timelock,
244 subaddress,
245 payment_id,
246 arbitrary_data: extra.data(),
247 },
248 });
249
250 break;
253 }
254 }
255
256 Ok(Timelocked(res))
257 }
258
259 fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
260 let ScannableBlock { block, transactions, output_index_for_first_ringct_output } = block;
263 if block.transactions.len() != transactions.len() {
264 Err(ScanError::InvalidScannableBlock(
265 "scanning a ScannableBlock with more/less transactions than it should have",
266 ))?;
267 }
268 let Some(mut output_index_for_first_ringct_output) = output_index_for_first_ringct_output
269 else {
270 return Ok(Timelocked(vec![]));
271 };
272
273 if block.header.hardfork_version > 16 {
274 Err(ScanError::UnsupportedProtocol(block.header.hardfork_version))?;
275 }
276
277 let mut txs_with_hashes = vec![(
279 block.miner_transaction.hash(),
280 Transaction::<Pruned>::from(block.miner_transaction.clone()),
281 )];
282 for (hash, tx) in block.transactions.iter().zip(transactions) {
283 txs_with_hashes.push((*hash, tx));
284 }
285
286 let mut res = Timelocked(vec![]);
287 for (hash, tx) in txs_with_hashes {
288 {
290 let mut this_txs_outputs = vec![];
291 core::mem::swap(
292 &mut self.scan_transaction(output_index_for_first_ringct_output, hash, &tx)?.0,
293 &mut this_txs_outputs,
294 );
295 res.0.extend(this_txs_outputs);
296 }
297
298 if matches!(tx, Transaction::V2 { .. }) {
300 output_index_for_first_ringct_output += u64::try_from(tx.prefix().outputs.len())
301 .expect("couldn't convert amount of outputs (usize) to u64")
302 }
303 }
304
305 if block.header.hardfork_version >= 12 {
309 for output in &mut res.0 {
310 if matches!(output.metadata.payment_id, Some(PaymentId::Unencrypted(_))) {
311 output.metadata.payment_id = None;
312 }
313 }
314 }
315
316 Ok(res)
317 }
318}
319
320#[derive(Clone, Zeroize, ZeroizeOnDrop)]
332pub struct Scanner(InternalScanner);
333
334impl Scanner {
335 pub fn new(pair: ViewPair) -> Self {
337 Self(InternalScanner::new(pair, false))
338 }
339
340 pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
344 self.0.register_subaddress(subaddress)
345 }
346
347 pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
349 self.0.scan(block)
350 }
351}
352
353#[derive(Clone, Zeroize, ZeroizeOnDrop)]
361pub struct GuaranteedScanner(InternalScanner);
362
363impl GuaranteedScanner {
364 pub fn new(pair: GuaranteedViewPair) -> Self {
366 Self(InternalScanner::new(pair.0, true))
367 }
368
369 pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
373 self.0.register_subaddress(subaddress)
374 }
375
376 pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
378 self.0.scan(block)
379 }
380}