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;
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<CompressedPoint, 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().into(), 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().into(), 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(&mut tx.prefix().extra.as_slice()) 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) = output.key.decompress() 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::<CompressedPoint>(&subaddress_spend_key.compress().into())
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(&CompressedPoint::from(commitment.calculate().compress())) !=
223 proofs.base.commitments.get(o)
224 {
225 continue;
226 }
227 }
228
229 let payment_id = payment_id.map(|id| id ^ SharedKeyDerivations::payment_id_xor(ecdh));
231
232 let o = u64::try_from(o).expect("couldn't convert output index (usize) to u64");
233
234 res.push(WalletOutput {
235 absolute_id: AbsoluteId { transaction: tx_hash, index_in_transaction: o },
236 relative_id: RelativeId {
237 index_on_blockchain: output_index_for_first_ringct_output.checked_add(o).ok_or(
238 ScanError::InvalidScannableBlock(
239 "transaction's output's index isn't representable as a u64",
240 ),
241 )?,
242 },
243 data: OutputData { key: output_key, key_offset, commitment },
244 metadata: Metadata {
245 additional_timelock: tx.prefix().additional_timelock,
246 subaddress,
247 payment_id,
248 arbitrary_data: extra.arbitrary_data(),
249 },
250 });
251
252 break;
255 }
256 }
257
258 Ok(Timelocked(res))
259 }
260
261 fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
262 let ScannableBlock { block, transactions, output_index_for_first_ringct_output } = block;
265 if block.transactions.len() != transactions.len() {
266 Err(ScanError::InvalidScannableBlock(
267 "scanning a ScannableBlock with more/less transactions than it should have",
268 ))?;
269 }
270 let Some(mut output_index_for_first_ringct_output) = output_index_for_first_ringct_output
271 else {
272 return Ok(Timelocked(vec![]));
273 };
274
275 if block.header.hardfork_version > 16 {
276 Err(ScanError::UnsupportedProtocol(block.header.hardfork_version))?;
277 }
278
279 let mut txs_with_hashes = vec![(
281 block.miner_transaction().hash(),
282 Transaction::<Pruned>::from(block.miner_transaction().clone()),
283 )];
284 for (hash, tx) in block.transactions.iter().zip(transactions) {
285 txs_with_hashes.push((*hash, tx));
286 }
287
288 let mut res = Timelocked(vec![]);
289 for (hash, tx) in txs_with_hashes {
290 {
292 let mut this_txs_outputs = vec![];
293 core::mem::swap(
294 &mut self.scan_transaction(output_index_for_first_ringct_output, hash, &tx)?.0,
295 &mut this_txs_outputs,
296 );
297 res.0.extend(this_txs_outputs);
298 }
299
300 if matches!(tx, Transaction::V2 { .. }) {
302 output_index_for_first_ringct_output += u64::try_from(tx.prefix().outputs.len())
303 .expect("couldn't convert amount of outputs (usize) to u64")
304 }
305 }
306
307 if block.header.hardfork_version >= 12 {
311 for output in &mut res.0 {
312 if matches!(output.metadata.payment_id, Some(PaymentId::Unencrypted(_))) {
313 output.metadata.payment_id = None;
314 }
315 }
316 }
317
318 Ok(res)
319 }
320}
321
322#[derive(Clone, Zeroize, ZeroizeOnDrop)]
334pub struct Scanner(InternalScanner);
335
336impl Scanner {
337 pub fn new(pair: ViewPair) -> Self {
339 Self(InternalScanner::new(pair, false))
340 }
341
342 pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
346 self.0.register_subaddress(subaddress)
347 }
348
349 pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
351 self.0.scan(block)
352 }
353}
354
355#[derive(Clone, Zeroize, ZeroizeOnDrop)]
363pub struct GuaranteedScanner(InternalScanner);
364
365impl GuaranteedScanner {
366 pub fn new(pair: GuaranteedViewPair) -> Self {
368 Self(InternalScanner::new(pair.0, true))
369 }
370
371 pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
375 self.0.register_subaddress(subaddress)
376 }
377
378 pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
380 self.0.scan(block)
381 }
382}