1use core::ops::Deref as _;
2use std_shims::{vec, vec::Vec, collections::HashMap};
3
4use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
5
6#[cfg(feature = "compile-time-generators")]
7use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
8#[cfg(not(feature = "compile-time-generators"))]
9use curve25519_dalek::constants::ED25519_BASEPOINT_POINT as ED25519_BASEPOINT_TABLE;
10
11use monero_oxide::{
12 ed25519::{Scalar, CompressedPoint, Point, Commitment},
13 transaction::{Timelock, Pruned, Transaction},
14};
15use monero_interface::ScannableBlock;
16use crate::{
17 address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra,
18 SharedKeyDerivations,
19};
20
21#[derive(Zeroize, ZeroizeOnDrop)]
23pub struct Timelocked(Vec<WalletOutput>);
24
25impl Timelocked {
26 #[must_use]
28 pub fn not_additionally_locked(self) -> Vec<WalletOutput> {
29 let mut res = vec![];
30 for output in &self.0 {
31 if output.additional_timelock() == Timelock::None {
32 res.push(output.clone());
33 }
34 }
35 res
36 }
37
38 #[must_use]
50 pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
51 let mut res = vec![];
52 for output in &self.0 {
53 if (output.additional_timelock() <= Timelock::Block(block)) ||
54 (output.additional_timelock() <= Timelock::Time(time))
55 {
56 res.push(output.clone());
57 }
58 }
59 res
60 }
61
62 #[must_use]
64 pub fn ignore_additional_timelock(mut self) -> Vec<WalletOutput> {
65 let mut res = vec![];
66 core::mem::swap(&mut self.0, &mut res);
67 res
68 }
69}
70
71#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
73pub enum ScanError {
74 #[error("unsupported protocol version ({0})")]
76 UnsupportedProtocol(u8),
77 #[error("invalid scannable block ({0})")]
79 InvalidScannableBlock(&'static str),
80}
81
82#[derive(Clone)]
83struct InternalScanner {
84 pair: ViewPair,
85 guaranteed: bool,
86 subaddresses: HashMap<CompressedPoint, Option<SubaddressIndex>>,
87}
88
89impl Zeroize for InternalScanner {
90 #[expect(clippy::iter_over_hash_type)]
91 fn zeroize(&mut self) {
92 self.pair.zeroize();
93 self.guaranteed.zeroize();
94
95 for (mut key, mut value) in self.subaddresses.drain() {
97 key.zeroize();
98 value.zeroize();
99 }
100 }
101}
102impl Drop for InternalScanner {
103 fn drop(&mut self) {
104 self.zeroize();
105 }
106}
107impl ZeroizeOnDrop for InternalScanner {}
108
109impl InternalScanner {
110 fn new(pair: ViewPair, guaranteed: bool) -> Self {
111 let mut subaddresses = HashMap::new();
112 subaddresses.insert(pair.spend().compress(), None);
113 Self { pair, guaranteed, subaddresses }
114 }
115
116 fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
117 let (spend, _) = self.pair.subaddress_keys(subaddress);
118 self.subaddresses.insert(spend.compress(), Some(subaddress));
119 }
120
121 fn scan_transaction(
122 &self,
123 output_index_for_first_ringct_output: u64,
124 tx_hash: [u8; 32],
125 tx: &Transaction<Pruned>,
126 ) -> Result<Timelocked, ScanError> {
127 if tx.version() != 2 {
130 return Ok(Timelocked(vec![]));
131 }
132
133 let Ok(extra) = Extra::read(&mut tx.prefix().extra.as_slice()) else {
135 return Ok(Timelocked(vec![]));
136 };
137
138 let Some((tx_keys, additional)) = extra.keys() else {
139 return Ok(Timelocked(vec![]));
140 };
141 let payment_id = extra.payment_id();
142
143 let mut res = vec![];
144 for (o, output) in tx.prefix().outputs.iter().enumerate() {
145 if output.key == CompressedPoint::IDENTITY {
153 continue;
154 }
155
156 let Some(output_key) = output.key.decompress() else { continue };
157
158 let additional = additional.as_ref().and_then(|additional| additional.get(o));
166
167 for key in tx_keys.iter().map(Some).chain(core::iter::once(additional)).flatten().copied() {
168 let ecdh = {
170 let dalek_view = Zeroizing::new((*self.pair.view).into());
171 Zeroizing::new(Point::from(dalek_view.deref() * key.into()))
172 };
173 let output_derivations = SharedKeyDerivations::output_derivations(
174 self.guaranteed.then(|| SharedKeyDerivations::uniqueness(&tx.prefix().inputs)),
175 ecdh.clone(),
176 o,
177 );
178
179 if let Some(actual_view_tag) = output.view_tag {
181 if actual_view_tag != output_derivations.view_tag {
182 continue;
183 }
184 }
185
186 let Some(subaddress) = ({
188 let subaddress_spend_key =
193 output_key.into() - (&output_derivations.shared_key.into() * ED25519_BASEPOINT_TABLE);
194 self
195 .subaddresses
196 .get::<CompressedPoint>(&subaddress_spend_key.compress().to_bytes().into())
197 }) else {
198 continue;
199 };
200 let subaddress = *subaddress;
201
202 let mut key_offset = output_derivations.shared_key.into();
204 if let Some(subaddress) = subaddress {
205 key_offset += self.pair.subaddress_derivation(subaddress).into();
208 }
209 let mut commitment = Commitment::zero();
211
212 if let Some(amount) = output.amount {
214 commitment.amount = amount;
215 } else {
217 let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
218 Err(ScanError::InvalidScannableBlock("non-miner v2 transaction without RCT proofs"))?
220 };
221
222 commitment = match proofs.base.encrypted_amounts.get(o) {
223 Some(amount) => output_derivations.decrypt(amount),
224 None => Err(ScanError::InvalidScannableBlock(
226 "RCT proofs without an encrypted amount per output",
227 ))?,
228 };
229
230 if Some(&commitment.commit().compress()) != proofs.base.commitments.get(o) {
232 continue;
233 }
234 }
235
236 let payment_id = payment_id.map(|id| id ^ SharedKeyDerivations::payment_id_xor(ecdh));
238
239 let o = u64::try_from(o).expect("couldn't convert output index (usize) to u64");
240
241 res.push(WalletOutput {
242 absolute_id: AbsoluteId { transaction: tx_hash, index_in_transaction: o },
243 relative_id: RelativeId {
244 index_on_blockchain: output_index_for_first_ringct_output.checked_add(o).ok_or(
245 ScanError::InvalidScannableBlock(
246 "transaction's output's index isn't representable as a u64",
247 ),
248 )?,
249 },
250 data: OutputData { key: output_key, key_offset: Scalar::from(key_offset), commitment },
251 metadata: Metadata {
252 additional_timelock: tx.prefix().additional_timelock,
253 subaddress,
254 payment_id,
255 arbitrary_data: extra.arbitrary_data(),
256 },
257 });
258
259 break;
262 }
263 }
264
265 Ok(Timelocked(res))
266 }
267
268 fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
269 let ScannableBlock { block, transactions, output_index_for_first_ringct_output } = block;
272 if block.transactions.len() != transactions.len() {
273 Err(ScanError::InvalidScannableBlock(
274 "scanning a ScannableBlock with more/less transactions than it should have",
275 ))?;
276 }
277 let Some(mut output_index_for_first_ringct_output) = output_index_for_first_ringct_output
278 else {
279 return Ok(Timelocked(vec![]));
280 };
281
282 if block.header.hardfork_version > 16 {
283 Err(ScanError::UnsupportedProtocol(block.header.hardfork_version))?;
284 }
285
286 let mut txs_with_hashes = vec![(
288 block.miner_transaction().hash(),
289 Transaction::<Pruned>::from(block.miner_transaction().clone()),
290 )];
291 for (hash, tx) in block.transactions.iter().zip(transactions) {
292 txs_with_hashes.push((*hash, tx));
293 }
294
295 let mut res = Timelocked(vec![]);
296 for (hash, tx) in txs_with_hashes {
297 {
299 let mut this_txs_outputs = vec![];
300 core::mem::swap(
301 &mut self.scan_transaction(output_index_for_first_ringct_output, hash, &tx)?.0,
302 &mut this_txs_outputs,
303 );
304 res.0.extend(this_txs_outputs);
305 }
306
307 if matches!(tx, Transaction::V2 { .. }) {
309 output_index_for_first_ringct_output = output_index_for_first_ringct_output
310 .checked_add(
311 u64::try_from(tx.prefix().outputs.len())
312 .expect("couldn't convert amount of outputs (usize) to u64"),
313 )
314 .ok_or(ScanError::InvalidScannableBlock("RingCT output indexes exceeded u64::MAX"))?;
315 }
316 }
317
318 if block.header.hardfork_version >= 12 {
322 for output in &mut res.0 {
323 if matches!(output.metadata.payment_id, Some(PaymentId::Unencrypted(_))) {
324 output.metadata.payment_id = None;
325 }
326 }
327 }
328
329 Ok(res)
330 }
331}
332
333#[derive(Clone, Zeroize, ZeroizeOnDrop)]
345pub struct Scanner(InternalScanner);
346
347impl Scanner {
348 pub fn new(pair: ViewPair) -> Self {
350 Self(InternalScanner::new(pair, false))
351 }
352
353 pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
360 self.0.register_subaddress(subaddress);
361 }
362
363 pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
368 self.0.scan(block)
369 }
370}
371
372#[derive(Clone, Zeroize, ZeroizeOnDrop)]
380pub struct GuaranteedScanner(InternalScanner);
381
382impl GuaranteedScanner {
383 pub fn new(pair: GuaranteedViewPair) -> Self {
385 Self(InternalScanner::new(pair.0, true))
386 }
387
388 pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
395 self.0.register_subaddress(subaddress);
396 }
397
398 pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
403 self.0.scan(block)
404 }
405}