monero_rpc/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![cfg_attr(not(feature = "std"), no_std)]
5
6use core::{
7  future::Future,
8  fmt::Debug,
9  ops::{Bound, RangeBounds},
10};
11use std_shims::{
12  alloc::format,
13  vec,
14  vec::Vec,
15  io,
16  string::{String, ToString},
17};
18
19use zeroize::Zeroize;
20
21use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
22
23use serde::{Serialize, Deserialize, de::DeserializeOwned};
24use serde_json::{Value, json};
25
26use monero_oxide::{
27  io::*,
28  transaction::{Input, Timelock, Pruned, Transaction},
29  block::Block,
30  DEFAULT_LOCK_WINDOW,
31};
32use monero_address::Address;
33
34// Number of blocks the fee estimate will be valid for
35// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c
36//   /src/wallet/wallet2.cpp#L121
37const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10;
38
39// Monero errors if more than 100 is requested unless using a non-restricted RPC
40// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
41//   /src/rpc/core_rpc_server.cpp#L75
42const TXS_PER_REQUEST: usize = 100;
43
44/// An error from the RPC.
45#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
46pub enum RpcError {
47  /// An internal error.
48  #[error("internal error ({0})")]
49  InternalError(String),
50  /// A connection error with the node.
51  #[error("connection error ({0})")]
52  ConnectionError(String),
53  /// The node is invalid per the expected protocol.
54  #[error("invalid node ({0})")]
55  InvalidNode(String),
56  /// Requested transactions weren't found.
57  #[error("transactions not found")]
58  TransactionsNotFound(Vec<[u8; 32]>),
59  /// The transaction was pruned.
60  ///
61  /// Pruned transactions are not supported at this time.
62  #[error("pruned transaction")]
63  PrunedTransaction,
64  /// A transaction (sent or received) was invalid.
65  #[error("invalid transaction ({0:?})")]
66  InvalidTransaction([u8; 32]),
67  /// The returned fee was unusable.
68  #[error("unexpected fee response")]
69  InvalidFee,
70  /// The priority intended for use wasn't usable.
71  #[error("invalid priority")]
72  InvalidPriority,
73}
74
75/// A block which is able to be scanned.
76#[derive(Clone, PartialEq, Eq, Debug)]
77pub struct ScannableBlock {
78  /// The block which is being scanned.
79  pub block: Block,
80  /// The non-miner transactions within this block.
81  pub transactions: Vec<Transaction<Pruned>>,
82  /// The output index for the first RingCT output within this block.
83  ///
84  /// None if there are no RingCT outputs within this block, Some otherwise.
85  pub output_index_for_first_ringct_output: Option<u64>,
86}
87
88/// A struct containing a fee rate.
89///
90/// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes.
91#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
92pub struct FeeRate {
93  /// The fee per-weight of the transaction.
94  per_weight: u64,
95  /// The mask to round with.
96  mask: u64,
97}
98
99impl FeeRate {
100  /// Construct a new fee rate.
101  pub fn new(per_weight: u64, mask: u64) -> Result<FeeRate, RpcError> {
102    if (per_weight == 0) || (mask == 0) {
103      Err(RpcError::InvalidFee)?;
104    }
105    Ok(FeeRate { per_weight, mask })
106  }
107
108  /// Write the FeeRate.
109  ///
110  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
111  /// defined serialization.
112  pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
113    w.write_all(&self.per_weight.to_le_bytes())?;
114    w.write_all(&self.mask.to_le_bytes())
115  }
116
117  /// Serialize the FeeRate to a `Vec<u8>`.
118  ///
119  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
120  /// defined serialization.
121  pub fn serialize(&self) -> Vec<u8> {
122    let mut res = Vec::with_capacity(16);
123    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
124    res
125  }
126
127  /// Read a FeeRate.
128  ///
129  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
130  /// defined serialization.
131  pub fn read(r: &mut impl io::Read) -> io::Result<FeeRate> {
132    let per_weight = read_u64(r)?;
133    let mask = read_u64(r)?;
134    FeeRate::new(per_weight, mask).map_err(io::Error::other)
135  }
136
137  /// Calculate the fee to use from the weight.
138  ///
139  /// This function may panic upon overflow.
140  pub fn calculate_fee_from_weight(&self, weight: usize) -> u64 {
141    let fee =
142      self.per_weight * u64::try_from(weight).expect("couldn't convert weight (usize) to u64");
143    let fee = fee.div_ceil(self.mask) * self.mask;
144    debug_assert_eq!(
145      Some(weight),
146      self.calculate_weight_from_fee(fee),
147      "Miscalculated weight from fee"
148    );
149    fee
150  }
151
152  /// Calculate the weight from the fee.
153  ///
154  /// Returns `None` if the weight would not fit within a `usize`.
155  pub fn calculate_weight_from_fee(&self, fee: u64) -> Option<usize> {
156    usize::try_from(fee / self.per_weight).ok()
157  }
158}
159
160/// The priority for the fee.
161///
162/// Higher-priority transactions will be included in blocks earlier.
163#[derive(Clone, Copy, PartialEq, Eq, Debug)]
164#[allow(non_camel_case_types)]
165pub enum FeePriority {
166  /// The `Unimportant` priority, as defined by Monero.
167  Unimportant,
168  /// The `Normal` priority, as defined by Monero.
169  Normal,
170  /// The `Elevated` priority, as defined by Monero.
171  Elevated,
172  /// The `Priority` priority, as defined by Monero.
173  Priority,
174  /// A custom priority.
175  Custom {
176    /// The numeric representation of the priority, as used within the RPC.
177    priority: u32,
178  },
179}
180
181/// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
182///   src/simplewallet/simplewallet.cpp#L161
183impl FeePriority {
184  pub(crate) fn fee_priority(&self) -> u32 {
185    match self {
186      FeePriority::Unimportant => 1,
187      FeePriority::Normal => 2,
188      FeePriority::Elevated => 3,
189      FeePriority::Priority => 4,
190      FeePriority::Custom { priority, .. } => *priority,
191    }
192  }
193}
194
195#[derive(Debug, Deserialize)]
196struct JsonRpcResponse<T> {
197  result: T,
198}
199
200#[derive(Debug, Deserialize)]
201struct TransactionResponse {
202  tx_hash: String,
203  as_hex: String,
204  pruned_as_hex: String,
205}
206#[derive(Debug, Deserialize)]
207struct TransactionsResponse {
208  #[serde(default)]
209  missed_tx: Vec<String>,
210  txs: Vec<TransactionResponse>,
211}
212
213/// The response to an query for the information of a RingCT output.
214#[derive(Clone, Copy, PartialEq, Eq, Debug)]
215pub struct OutputInformation {
216  /// The block number of the block this output was added to the chain in.
217  ///
218  /// This is equivalent to he height of the blockchain at the time the block was added.
219  pub height: usize,
220  /// If the output is unlocked, per the node's local view.
221  pub unlocked: bool,
222  /// The output's key.
223  ///
224  /// This is a CompressedEdwardsY, not an EdwardsPoint, as it may be invalid. CompressedEdwardsY
225  /// only asserts validity on decompression and allows representing compressed types.
226  pub key: CompressedEdwardsY,
227  /// The output's commitment.
228  pub commitment: EdwardsPoint,
229  /// The transaction which created this output.
230  pub transaction: [u8; 32],
231}
232
233fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
234  hex::decode(value).map_err(|_| RpcError::InvalidNode("expected hex wasn't hex".to_string()))
235}
236
237fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> {
238  rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode("hash wasn't 32-bytes".to_string()))
239}
240
241fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
242  CompressedPoint(
243    rpc_hex(point)?
244      .try_into()
245      .map_err(|_| RpcError::InvalidNode(format!("invalid point: {point}")))?,
246  )
247  .decompress()
248  .ok_or_else(|| RpcError::InvalidNode(format!("invalid point: {point}")))
249}
250
251/// An RPC connection to a Monero daemon.
252///
253/// This is abstract such that users can use an HTTP library (which being their choice), a
254/// Tor/i2p-based transport, or even a memory buffer an external service somehow routes.
255///
256/// While no implementors are directly provided, [monero-simple-request-rpc](
257///   https://github.com/monero-oxide/monero-oxide/tree/main/monero-oxide/rpc/simple-request
258/// ) is recommended.
259pub trait Rpc: Sync + Clone {
260  /// Perform a POST request to the specified route with the specified body.
261  ///
262  /// The implementor is left to handle anything such as authentication.
263  fn post(
264    &self,
265    route: &str,
266    body: Vec<u8>,
267  ) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>>;
268
269  /// Perform a RPC call to the specified route with the provided parameters.
270  ///
271  /// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
272  /// `json_rpc_call`.
273  fn rpc_call<Params: Send + Serialize + Debug, Response: DeserializeOwned + Debug>(
274    &self,
275    route: &str,
276    params: Option<Params>,
277  ) -> impl Send + Future<Output = Result<Response, RpcError>> {
278    async move {
279      let res = self
280        .post(
281          route,
282          if let Some(params) = params.as_ref() {
283            serde_json::to_string(params)
284              .map_err(|e| {
285                RpcError::InternalError(format!(
286                  "couldn't convert parameters ({params:?}) to JSON: {e:?}"
287                ))
288              })?
289              .into_bytes()
290          } else {
291            vec![]
292          },
293        )
294        .await?;
295      let res_str = std_shims::str::from_utf8(&res)
296        .map_err(|_| RpcError::InvalidNode("response wasn't utf-8".to_string()))?;
297      serde_json::from_str(res_str)
298        .map_err(|_| RpcError::InvalidNode(format!("response wasn't the expected json: {res_str}")))
299    }
300  }
301
302  /// Perform a JSON-RPC call with the specified method with the provided parameters.
303  fn json_rpc_call<Response: DeserializeOwned + Debug>(
304    &self,
305    method: &str,
306    params: Option<Value>,
307  ) -> impl Send + Future<Output = Result<Response, RpcError>> {
308    async move {
309      let mut req = json!({ "method": method });
310      if let Some(params) = params {
311        req
312          .as_object_mut()
313          .expect("accessing object as object failed?")
314          .insert("params".into(), params);
315      }
316      Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
317    }
318  }
319
320  /// Perform a binary call to the specified route with the provided parameters.
321  fn bin_call(
322    &self,
323    route: &str,
324    params: Vec<u8>,
325  ) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>> {
326    async move { self.post(route, params).await }
327  }
328
329  /// Get the active blockchain protocol version.
330  ///
331  /// This is specifically the major version within the most recent block header.
332  fn get_hardfork_version(&self) -> impl Send + Future<Output = Result<u8, RpcError>> {
333    async move {
334      #[derive(Debug, Deserialize)]
335      struct HeaderResponse {
336        major_version: u8,
337      }
338
339      #[derive(Debug, Deserialize)]
340      struct LastHeaderResponse {
341        block_header: HeaderResponse,
342      }
343
344      Ok(
345        self
346          .json_rpc_call::<LastHeaderResponse>("get_last_block_header", None)
347          .await?
348          .block_header
349          .major_version,
350      )
351    }
352  }
353
354  /// Get the height of the Monero blockchain.
355  ///
356  /// The height is defined as the amount of blocks on the blockchain. For a blockchain with only
357  /// its genesis block, the height will be 1.
358  fn get_height(&self) -> impl Send + Future<Output = Result<usize, RpcError>> {
359    async move {
360      #[derive(Debug, Deserialize)]
361      struct HeightResponse {
362        height: usize,
363      }
364      let res = self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height;
365      if res == 0 {
366        Err(RpcError::InvalidNode("node responded with 0 for the height".to_string()))?;
367      }
368      Ok(res)
369    }
370  }
371
372  /// Get the specified transactions.
373  ///
374  /// The received transactions will be hashed in order to verify the correct transactions were
375  /// returned.
376  fn get_transactions(
377    &self,
378    hashes: &[[u8; 32]],
379  ) -> impl Send + Future<Output = Result<Vec<Transaction>, RpcError>> {
380    async move {
381      if hashes.is_empty() {
382        return Ok(vec![]);
383      }
384
385      let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
386      let mut all_txs = Vec::with_capacity(hashes.len());
387      while !hashes_hex.is_empty() {
388        let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
389
390        let txs: TransactionsResponse = self
391          .rpc_call(
392            "get_transactions",
393            Some(json!({
394              "txs_hashes": hashes_hex.drain(.. this_count).collect::<Vec<_>>(),
395            })),
396          )
397          .await?;
398
399        if !txs.missed_tx.is_empty() {
400          Err(RpcError::TransactionsNotFound(
401            txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
402          ))?;
403        }
404        if txs.txs.len() != this_count {
405          Err(RpcError::InvalidNode(
406            "not missing any transactions yet didn't return all transactions".to_string(),
407          ))?;
408        }
409
410        all_txs.extend(txs.txs);
411      }
412
413      all_txs
414        .iter()
415        .enumerate()
416        .map(|(i, res)| {
417          // https://github.com/monero-project/monero/issues/8311
418          let buf = rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?;
419          let mut buf = buf.as_slice();
420          let tx = Transaction::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) {
421            Ok(hash) => RpcError::InvalidTransaction(hash),
422            Err(err) => err,
423          })?;
424          if !buf.is_empty() {
425            Err(RpcError::InvalidNode("transaction had extra bytes after it".to_string()))?;
426          }
427
428          // We check this to ensure we didn't read a pruned transaction when we meant to read an
429          // actual transaction. That shouldn't be possible, as they have different serializations,
430          // yet it helps to ensure that if we applied the above exception (using the pruned data),
431          // it was for the right reason
432          if res.as_hex.is_empty() {
433            match tx.prefix().inputs.first() {
434              Some(Input::Gen { .. }) => (),
435              _ => Err(RpcError::PrunedTransaction)?,
436            }
437          }
438
439          // This does run a few keccak256 hashes, which is pointless if the node is trusted
440          // In exchange, this provides resilience against invalid/malicious nodes
441          if tx.hash() != hashes[i] {
442            Err(RpcError::InvalidNode(
443              "replied with transaction wasn't the requested transaction".to_string(),
444            ))?;
445          }
446
447          Ok(tx)
448        })
449        .collect()
450    }
451  }
452
453  /// Get the specified transactions in their pruned format.
454  fn get_pruned_transactions(
455    &self,
456    hashes: &[[u8; 32]],
457  ) -> impl Send + Future<Output = Result<Vec<Transaction<Pruned>>, RpcError>> {
458    async move {
459      if hashes.is_empty() {
460        return Ok(vec![]);
461      }
462
463      let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
464      let mut all_txs = Vec::with_capacity(hashes.len());
465      while !hashes_hex.is_empty() {
466        let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
467
468        let txs: TransactionsResponse = self
469          .rpc_call(
470            "get_transactions",
471            Some(json!({
472              "txs_hashes": hashes_hex.drain(.. this_count).collect::<Vec<_>>(),
473              "prune": true,
474            })),
475          )
476          .await?;
477
478        if !txs.missed_tx.is_empty() {
479          Err(RpcError::TransactionsNotFound(
480            txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
481          ))?;
482        }
483
484        all_txs.extend(txs.txs);
485      }
486
487      all_txs
488        .iter()
489        .map(|res| {
490          let buf = rpc_hex(&res.pruned_as_hex)?;
491          let mut buf = buf.as_slice();
492          let tx =
493            Transaction::<Pruned>::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) {
494              Ok(hash) => RpcError::InvalidTransaction(hash),
495              Err(err) => err,
496            })?;
497          if !buf.is_empty() {
498            Err(RpcError::InvalidNode("pruned transaction had extra bytes after it".to_string()))?;
499          }
500          Ok(tx)
501        })
502        .collect()
503    }
504  }
505
506  /// Get the specified transaction.
507  ///
508  /// The received transaction will be hashed in order to verify the correct transaction was
509  /// returned.
510  fn get_transaction(
511    &self,
512    tx: [u8; 32],
513  ) -> impl Send + Future<Output = Result<Transaction, RpcError>> {
514    async move { self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) }
515  }
516
517  /// Get the specified transaction in its pruned format.
518  fn get_pruned_transaction(
519    &self,
520    tx: [u8; 32],
521  ) -> impl Send + Future<Output = Result<Transaction<Pruned>, RpcError>> {
522    async move { self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) }
523  }
524
525  /// Get the hash of a block from the node.
526  ///
527  /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
528  /// `height - 1` for the latest block).
529  fn get_block_hash(
530    &self,
531    number: usize,
532  ) -> impl Send + Future<Output = Result<[u8; 32], RpcError>> {
533    async move {
534      #[derive(Debug, Deserialize)]
535      struct BlockHeaderResponse {
536        hash: String,
537      }
538      #[derive(Debug, Deserialize)]
539      struct BlockHeaderByHeightResponse {
540        block_header: BlockHeaderResponse,
541      }
542
543      let header: BlockHeaderByHeightResponse =
544        self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?;
545      hash_hex(&header.block_header.hash)
546    }
547  }
548
549  /// Get a block from the node by its hash.
550  ///
551  /// The received block will be hashed in order to verify the correct block was returned.
552  fn get_block(&self, hash: [u8; 32]) -> impl Send + Future<Output = Result<Block, RpcError>> {
553    async move {
554      #[derive(Debug, Deserialize)]
555      struct BlockResponse {
556        blob: String,
557      }
558
559      let res: BlockResponse =
560        self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?;
561
562      let block = Block::read(&mut rpc_hex(&res.blob)?.as_slice())
563        .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
564      if block.hash() != hash {
565        Err(RpcError::InvalidNode("different block than requested (hash)".to_string()))?;
566      }
567      Ok(block)
568    }
569  }
570
571  /// Get a block from the node by its number.
572  ///
573  /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
574  /// `height - 1` for the latest block).
575  fn get_block_by_number(
576    &self,
577    number: usize,
578  ) -> impl Send + Future<Output = Result<Block, RpcError>> {
579    async move {
580      #[derive(Debug, Deserialize)]
581      struct BlockResponse {
582        blob: String,
583      }
584
585      let res: BlockResponse =
586        self.json_rpc_call("get_block", Some(json!({ "height": number }))).await?;
587
588      let block = Block::read(&mut rpc_hex(&res.blob)?.as_slice())
589        .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
590
591      // Make sure this is actually the block for this number
592      match block.miner_transaction.prefix().inputs.first() {
593        Some(Input::Gen(actual)) => {
594          if *actual == number {
595            Ok(block)
596          } else {
597            Err(RpcError::InvalidNode("different block than requested (number)".to_string()))
598          }
599        }
600        _ => Err(RpcError::InvalidNode(
601          "block's miner_transaction didn't have an input of kind Input::Gen".to_string(),
602        )),
603      }
604    }
605  }
606
607  /// Get a block's scannable form.
608  fn get_scannable_block(
609    &self,
610    block: Block,
611  ) -> impl Send + Future<Output = Result<ScannableBlock, RpcError>> {
612    async move {
613      let transactions = self.get_pruned_transactions(&block.transactions).await?;
614
615      /*
616        Requesting the output index for each output we sucessfully scan would cause a loss of
617        privacy. We could instead request the output indexes for all outputs we scan, yet this
618        would notably increase the amount of RPC calls we make.
619
620        We solve this by requesting the output index for the first RingCT output in the block, which
621        should be within the miner transaction. Then, as we scan transactions, we update the output
622        index ourselves.
623
624        Please note we only will scan RingCT outputs so we only need to track the RingCT output
625        index. This decision was made due to spending CN outputs potentially having burdensome
626        requirements (the need to make a v1 TX due to insufficient decoys).
627
628        We bound ourselves to only scanning RingCT outputs by only scanning v2 transactions. This is
629        safe and correct since:
630
631        1) v1 transactions cannot create RingCT outputs.
632
633           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
634             /src/cryptonote_basic/cryptonote_format_utils.cpp#L866-L869
635
636        2) v2 miner transactions implicitly create RingCT outputs.
637
638           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
639             /src/blockchain_db/blockchain_db.cpp#L232-L241
640
641        3) v2 transactions must create RingCT outputs.
642
643           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
644             /src/cryptonote_core/blockchain.cpp#L3055-L3065
645
646           That does bound on the hard fork version being >= 3, yet all v2 TXs have a hard fork
647           version > 3.
648
649           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
650             /src/cryptonote_core/blockchain.cpp#L3417
651      */
652
653      // Get the index for the first output
654      let mut output_index_for_first_ringct_output = None;
655      let miner_tx_hash = block.miner_transaction.hash();
656      let miner_tx = Transaction::<Pruned>::from(block.miner_transaction.clone());
657      for (hash, tx) in core::iter::once((&miner_tx_hash, &miner_tx))
658        .chain(block.transactions.iter().zip(&transactions))
659      {
660        // If this isn't a RingCT output, or there are no outputs, move to the next TX
661        if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() {
662          continue;
663        }
664
665        let index = *self.get_o_indexes(*hash).await?.first().ok_or_else(|| {
666          RpcError::InvalidNode(
667            "requested output indexes for a TX with outputs and got none".to_string(),
668          )
669        })?;
670        output_index_for_first_ringct_output = Some(index);
671        break;
672      }
673
674      Ok(ScannableBlock { block, transactions, output_index_for_first_ringct_output })
675    }
676  }
677
678  /// Get a block's scannable form by its hash.
679  // TODO: get_blocks.bin
680  fn get_scannable_block_by_hash(
681    &self,
682    hash: [u8; 32],
683  ) -> impl Send + Future<Output = Result<ScannableBlock, RpcError>> {
684    async move { self.get_scannable_block(self.get_block(hash).await?).await }
685  }
686
687  /// Get a block's scannable form by its number.
688  // TODO: get_blocks_by_height.bin
689  fn get_scannable_block_by_number(
690    &self,
691    number: usize,
692  ) -> impl Send + Future<Output = Result<ScannableBlock, RpcError>> {
693    async move { self.get_scannable_block(self.get_block_by_number(number).await?).await }
694  }
695
696  /// Get the currently estimated fee rate from the node.
697  ///
698  /// This may be manipulated to unsafe levels and MUST be sanity checked.
699  ///
700  /// This MUST NOT be expected to be deterministic in any way.
701  fn get_fee_rate(
702    &self,
703    priority: FeePriority,
704  ) -> impl Send + Future<Output = Result<FeeRate, RpcError>> {
705    async move {
706      #[derive(Debug, Deserialize)]
707      struct FeeResponse {
708        status: String,
709        fees: Option<Vec<u64>>,
710        fee: u64,
711        quantization_mask: u64,
712      }
713
714      let res: FeeResponse = self
715        .json_rpc_call(
716          "get_fee_estimate",
717          Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })),
718        )
719        .await?;
720
721      if res.status != "OK" {
722        Err(RpcError::InvalidFee)?;
723      }
724
725      if let Some(fees) = res.fees {
726        // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
727        // src/wallet/wallet2.cpp#L7615-L7620
728        let priority_idx = usize::try_from(if priority.fee_priority() >= 4 {
729          3
730        } else {
731          priority.fee_priority().saturating_sub(1)
732        })
733        .map_err(|_| RpcError::InvalidPriority)?;
734
735        if priority_idx >= fees.len() {
736          Err(RpcError::InvalidPriority)
737        } else {
738          FeeRate::new(fees[priority_idx], res.quantization_mask)
739        }
740      } else {
741        // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
742        //   src/wallet/wallet2.cpp#L7569-L7584
743        // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
744        //   src/wallet/wallet2.cpp#L7660-L7661
745        let priority_idx = usize::try_from(if priority.fee_priority() == 0 {
746          1
747        } else {
748          priority.fee_priority() - 1
749        })
750        .map_err(|_| RpcError::InvalidPriority)?;
751        let multipliers = [1, 5, 25, 1000];
752        if priority_idx >= multipliers.len() {
753          // though not an RPC error, it seems sensible to treat as such
754          Err(RpcError::InvalidPriority)?;
755        }
756        let fee_multiplier = multipliers[priority_idx];
757
758        FeeRate::new(res.fee * fee_multiplier, res.quantization_mask)
759      }
760    }
761  }
762
763  /// Publish a transaction.
764  fn publish_transaction(
765    &self,
766    tx: &Transaction,
767  ) -> impl Send + Future<Output = Result<(), RpcError>> {
768    async move {
769      #[allow(dead_code)]
770      #[derive(Debug, Deserialize)]
771      struct SendRawResponse {
772        status: String,
773        double_spend: bool,
774        fee_too_low: bool,
775        invalid_input: bool,
776        invalid_output: bool,
777        low_mixin: bool,
778        not_relayed: bool,
779        overspend: bool,
780        too_big: bool,
781        too_few_outputs: bool,
782        reason: String,
783      }
784
785      let res: SendRawResponse = self
786        .rpc_call(
787          "send_raw_transaction",
788          Some(json!({ "tx_as_hex": hex::encode(tx.serialize()), "do_sanity_checks": false })),
789        )
790        .await?;
791
792      if res.status != "OK" {
793        Err(RpcError::InvalidTransaction(tx.hash()))?;
794      }
795
796      Ok(())
797    }
798  }
799
800  /// Generate blocks, with the specified address receiving the block reward.
801  ///
802  /// Returns the hashes of the generated blocks and the last block's number.
803  fn generate_blocks<const ADDR_BYTES: u128>(
804    &self,
805    address: &Address<ADDR_BYTES>,
806    block_count: usize,
807  ) -> impl Send + Future<Output = Result<(Vec<[u8; 32]>, usize), RpcError>> {
808    async move {
809      #[derive(Debug, Deserialize)]
810      struct BlocksResponse {
811        blocks: Vec<String>,
812        height: usize,
813      }
814
815      let res = self
816        .json_rpc_call::<BlocksResponse>(
817          "generateblocks",
818          Some(json!({
819            "wallet_address": address.to_string(),
820            "amount_of_blocks": block_count
821          })),
822        )
823        .await?;
824
825      let mut blocks = Vec::with_capacity(res.blocks.len());
826      for block in res.blocks {
827        blocks.push(hash_hex(&block)?);
828      }
829      Ok((blocks, res.height))
830    }
831  }
832
833  /// Get the output indexes of the specified transaction.
834  fn get_o_indexes(
835    &self,
836    hash: [u8; 32],
837  ) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>> {
838    async move {
839      // Given the immaturity of Rust epee libraries, this is a homegrown one which is only
840      // validated to work against this specific function
841
842      // Header for EPEE, an 8-byte magic and a version
843      const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
844
845      // Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol
846      fn read_epee_vi<R: io::Read>(reader: &mut R) -> io::Result<u64> {
847        let vi_start = read_byte(reader)?;
848        let len = match vi_start & 0b11 {
849          0 => 1,
850          1 => 2,
851          2 => 4,
852          3 => 8,
853          _ => unreachable!(),
854        };
855        let mut vi = u64::from(vi_start >> 2);
856        for i in 1 .. len {
857          vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6);
858        }
859        Ok(vi)
860      }
861
862      let mut request = EPEE_HEADER.to_vec();
863      // Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata)
864      request.push(1 << 2);
865      // Length of field name
866      request.push(4);
867      // Field name
868      request.extend(b"txid");
869      // Type of field
870      request.push(10);
871      // Length of string, since this byte array is technically a string
872      request.push(32 << 2);
873      // The "string"
874      request.extend(hash);
875
876      let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?;
877      let mut indexes = indexes_buf.as_slice();
878
879      (|| {
880        let mut res = None;
881        let mut has_status = false;
882
883        if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER {
884          Err(io::Error::other("invalid header"))?;
885        }
886
887        let read_object = |reader: &mut &[u8]| -> io::Result<Vec<u64>> {
888          // Read the amount of fields
889          let fields = read_byte(reader)? >> 2;
890
891          for _ in 0 .. fields {
892            // Read the length of the field's name
893            let name_len = read_byte(reader)?;
894            // Read the name of the field
895            let name = read_raw_vec(read_byte, name_len.into(), reader)?;
896
897            let type_with_array_flag = read_byte(reader)?;
898            // The type of this field, without the potentially set array flag
899            let kind = type_with_array_flag & (!0x80);
900            let has_array_flag = type_with_array_flag != kind;
901
902            // Read this many instances of the field
903            let iters = if has_array_flag { read_epee_vi(reader)? } else { 1 };
904
905            // Check the field type
906            {
907              #[allow(clippy::match_same_arms)]
908              let (expected_type, expected_array_flag) = match name.as_slice() {
909                b"o_indexes" => (5, true),
910                b"status" => (10, false),
911                b"untrusted" => (11, false),
912                b"credits" => (5, false),
913                b"top_hash" => (10, false),
914                // On-purposely prints name as a byte vector to prevent printing arbitrary strings
915                // This is a self-describing format so we don't have to error here, yet we don't
916                // claim this to be a complete deserialization function
917                // To ensure it works for this specific use case, it's best to ensure it's limited
918                // to this specific use case (ensuring we have less variables to deal with)
919                _ => {
920                  Err(io::Error::other(format!("unrecognized field in get_o_indexes: {name:?}")))?
921                }
922              };
923              if (expected_type != kind) || (expected_array_flag != has_array_flag) {
924                let fmt_array_bool = |array_bool| if array_bool { "array" } else { "not array" };
925                Err(io::Error::other(format!(
926                  "field {name:?} was {kind} ({}), expected {expected_type} ({})",
927                  fmt_array_bool(has_array_flag),
928                  fmt_array_bool(expected_array_flag)
929                )))?;
930              }
931            }
932
933            let read_field_as_bytes = match kind {
934              /*
935              // i64
936              1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
937              // i32
938              2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
939              // i16
940              3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
941              // i8
942              4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
943              */
944              // u64
945              5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
946              /*
947              // u32
948              6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
949              // u16
950              7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
951              // u8
952              8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
953              // double
954              9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
955              */
956              // string, or any collection of bytes
957              10 => |reader: &mut &[u8]| {
958                let len = read_epee_vi(reader)?;
959                read_raw_vec(
960                  read_byte,
961                  len.try_into().map_err(|_| io::Error::other("u64 length exceeded usize"))?,
962                  reader,
963                )
964              },
965              // bool
966              11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
967              /*
968              // object, errors here as it shouldn't be used on this call
969              12 => {
970                |_: &mut &[u8]| Err(io::Error::other("node used object in reply to get_o_indexes"))
971              }
972              // array, so far unused
973              13 => |_: &mut &[u8]| Err(io::Error::other("node used the unused array type")),
974              */
975              _ => |_: &mut &[u8]| Err(io::Error::other("node used an invalid type")),
976            };
977
978            let mut bytes_res = vec![];
979            for _ in 0 .. iters {
980              bytes_res.push(read_field_as_bytes(reader)?);
981            }
982
983            let mut actual_res = Vec::with_capacity(bytes_res.len());
984            match name.as_slice() {
985              b"o_indexes" => {
986                for o_index in bytes_res {
987                  actual_res.push(read_u64(&mut o_index.as_slice())?);
988                }
989                res = Some(actual_res);
990              }
991              b"status" => {
992                if bytes_res
993                  .first()
994                  .ok_or_else(|| io::Error::other("status was a 0-length array"))?
995                  .as_slice() !=
996                  b"OK"
997                {
998                  Err(io::Error::other("response wasn't OK"))?;
999                }
1000                has_status = true;
1001              }
1002              b"untrusted" | b"credits" | b"top_hash" => continue,
1003              _ => Err(io::Error::other("unrecognized field in get_o_indexes"))?,
1004            }
1005          }
1006
1007          if !has_status {
1008            Err(io::Error::other("response didn't contain a status"))?;
1009          }
1010
1011          // If the Vec was empty, it would've been omitted, hence the unwrap_or
1012          Ok(res.unwrap_or(vec![]))
1013        };
1014
1015        read_object(&mut indexes)
1016      })()
1017      .map_err(|e| RpcError::InvalidNode(format!("invalid binary response: {e:?}")))
1018    }
1019  }
1020}
1021
1022/// A trait for any object which can be used to select RingCT decoys.
1023///
1024/// An implementation is provided for any satisfier of `Rpc`. It is not recommended to use an `Rpc`
1025/// object to satisfy this. This should be satisfied by a local store of the output distribution,
1026/// both for performance and to prevent potential attacks a remote node can perform.
1027pub trait DecoyRpc: Sync {
1028  /// Get the height the output distribution ends at.
1029  ///
1030  /// This is equivalent to the height of the blockchain it's for. This is intended to be cheaper
1031  /// than fetching the entire output distribution.
1032  fn get_output_distribution_end_height(
1033    &self,
1034  ) -> impl Send + Future<Output = Result<usize, RpcError>>;
1035
1036  /// Get the RingCT (zero-amount) output distribution.
1037  ///
1038  /// `range` is in terms of block numbers. The result may be smaller than the requested range if
1039  /// the range starts before RingCT outputs were created on-chain.
1040  fn get_output_distribution(
1041    &self,
1042    range: impl Send + RangeBounds<usize>,
1043  ) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>>;
1044
1045  /// Get the specified outputs from the RingCT (zero-amount) pool.
1046  fn get_outs(
1047    &self,
1048    indexes: &[u64],
1049  ) -> impl Send + Future<Output = Result<Vec<OutputInformation>, RpcError>>;
1050
1051  /// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
1052  /// timelock has been satisfied.
1053  ///
1054  /// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
1055  /// Monero transactions.
1056  ///
1057  /// The node is trusted for if the output is unlocked unless `fingerprintable_deterministic` is
1058  /// set to true. If `fingerprintable_deterministic` is set to true, the node's local view isn't
1059  /// used, yet the transaction's timelock is checked to be unlocked at the specified `height`.
1060  /// This offers a deterministic decoy selection, yet is fingerprintable as time-based timelocks
1061  /// aren't evaluated (and considered locked, preventing their selection).
1062  fn get_unlocked_outputs(
1063    &self,
1064    indexes: &[u64],
1065    height: usize,
1066    fingerprintable_deterministic: bool,
1067  ) -> impl Send + Future<Output = Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>>;
1068}
1069
1070impl<R: Rpc> DecoyRpc for R {
1071  fn get_output_distribution_end_height(
1072    &self,
1073  ) -> impl Send + Future<Output = Result<usize, RpcError>> {
1074    async move { <Self as Rpc>::get_height(self).await }
1075  }
1076
1077  fn get_output_distribution(
1078    &self,
1079    range: impl Send + RangeBounds<usize>,
1080  ) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>> {
1081    async move {
1082      #[derive(Default, Debug, Deserialize)]
1083      struct Distribution {
1084        distribution: Vec<u64>,
1085        // A blockchain with just its genesis block has a height of 1
1086        start_height: usize,
1087      }
1088
1089      #[derive(Debug, Deserialize)]
1090      struct Distributions {
1091        distributions: [Distribution; 1],
1092        status: String,
1093      }
1094
1095      let from = match range.start_bound() {
1096        Bound::Included(from) => *from,
1097        Bound::Excluded(from) => from.checked_add(1).ok_or_else(|| {
1098          RpcError::InternalError("range's from wasn't representable".to_string())
1099        })?,
1100        Bound::Unbounded => 0,
1101      };
1102      let to = match range.end_bound() {
1103        Bound::Included(to) => *to,
1104        Bound::Excluded(to) => to
1105          .checked_sub(1)
1106          .ok_or_else(|| RpcError::InternalError("range's to wasn't representable".to_string()))?,
1107        Bound::Unbounded => self.get_height().await? - 1,
1108      };
1109      if from > to {
1110        Err(RpcError::InternalError(format!(
1111          "malformed range: inclusive start {from}, inclusive end {to}"
1112        )))?;
1113      }
1114
1115      let zero_zero_case = (from == 0) && (to == 0);
1116      let distributions: Distributions = self
1117        .json_rpc_call(
1118          "get_output_distribution",
1119          Some(json!({
1120            "binary": false,
1121            "amounts": [0],
1122            "cumulative": true,
1123            // These are actually block numbers, not heights
1124            "from_height": from,
1125            "to_height": if zero_zero_case { 1 } else { to },
1126          })),
1127        )
1128        .await?;
1129
1130      if distributions.status != "OK" {
1131        Err(RpcError::ConnectionError(
1132          "node couldn't service this request for the output distribution".to_string(),
1133        ))?;
1134      }
1135
1136      let mut distributions = distributions.distributions;
1137      let Distribution { start_height, mut distribution } = core::mem::take(&mut distributions[0]);
1138      // start_height is also actually a block number, and it should be at least `from`
1139      // It may be after depending on when these outputs first appeared on the blockchain
1140      // Unfortunately, we can't validate without a binary search to find the RingCT activation
1141      // block and an iterative search from there, so we solely sanity check it
1142      if start_height < from {
1143        Err(RpcError::InvalidNode(format!(
1144          "requested distribution from {from} and got from {start_height}"
1145        )))?;
1146      }
1147      // It shouldn't be after `to` though
1148      if start_height > to {
1149        Err(RpcError::InvalidNode(format!(
1150          "requested distribution to {to} and got from {start_height}"
1151        )))?;
1152      }
1153
1154      let expected_len = if zero_zero_case {
1155        2
1156      } else {
1157        (to - start_height).checked_add(1).ok_or_else(|| {
1158          RpcError::InternalError("expected length of distribution exceeded usize".to_string())
1159        })?
1160      };
1161      // Yet this is actually a height
1162      if expected_len != distribution.len() {
1163        Err(RpcError::InvalidNode(format!(
1164          "distribution length ({}) wasn't of the requested length ({})",
1165          distribution.len(),
1166          expected_len
1167        )))?;
1168      }
1169      // Requesting to = 0 returns the distribution for the entire chain
1170      // We work around this by requesting 0, 1 (yielding two blocks), then popping the second
1171      // block
1172      if zero_zero_case {
1173        distribution.pop();
1174      }
1175
1176      // Check the distribution monotonically increases
1177      {
1178        let mut monotonic = 0;
1179        for d in &distribution {
1180          if *d < monotonic {
1181            Err(RpcError::InvalidNode(
1182              "received output distribution didn't increase monotonically".to_string(),
1183            ))?;
1184          }
1185          monotonic = *d;
1186        }
1187      }
1188
1189      Ok(distribution)
1190    }
1191  }
1192
1193  fn get_outs(
1194    &self,
1195    indexes: &[u64],
1196  ) -> impl Send + Future<Output = Result<Vec<OutputInformation>, RpcError>> {
1197    async move {
1198      #[derive(Debug, Deserialize)]
1199      struct OutputResponse {
1200        height: usize,
1201        unlocked: bool,
1202        key: String,
1203        mask: String,
1204        txid: String,
1205      }
1206
1207      #[derive(Debug, Deserialize)]
1208      struct OutsResponse {
1209        status: String,
1210        outs: Vec<OutputResponse>,
1211      }
1212
1213      // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
1214      //   /src/rpc/core_rpc_server.cpp#L67
1215      const MAX_OUTS: usize = 5000;
1216
1217      let mut res = Vec::with_capacity(indexes.len());
1218      for indexes in indexes.chunks(MAX_OUTS) {
1219        let rpc_res: OutsResponse = self
1220          .rpc_call(
1221            "get_outs",
1222            Some(json!({
1223              "get_txid": true,
1224              "outputs": indexes.iter().map(|o| json!({
1225                "amount": 0,
1226                "index": o
1227              })).collect::<Vec<_>>()
1228            })),
1229          )
1230          .await?;
1231
1232        if rpc_res.status != "OK" {
1233          Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?;
1234        }
1235
1236        res.extend(
1237          rpc_res
1238            .outs
1239            .into_iter()
1240            .map(|output| {
1241              Ok(OutputInformation {
1242                height: output.height,
1243                unlocked: output.unlocked,
1244                key: CompressedEdwardsY(
1245                  rpc_hex(&output.key)?
1246                    .try_into()
1247                    .map_err(|_| RpcError::InvalidNode("output key wasn't 32 bytes".to_string()))?,
1248                ),
1249                commitment: rpc_point(&output.mask)?,
1250                transaction: hash_hex(&output.txid)?,
1251              })
1252            })
1253            .collect::<Result<Vec<_>, RpcError>>()?,
1254        );
1255      }
1256
1257      Ok(res)
1258    }
1259  }
1260
1261  fn get_unlocked_outputs(
1262    &self,
1263    indexes: &[u64],
1264    height: usize,
1265    fingerprintable_deterministic: bool,
1266  ) -> impl Send + Future<Output = Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>> {
1267    async move {
1268      let outs = self.get_outs(indexes).await?;
1269
1270      // Only need to fetch txs to do deterministic check on timelock
1271      let txs = if fingerprintable_deterministic {
1272        self.get_transactions(&outs.iter().map(|out| out.transaction).collect::<Vec<_>>()).await?
1273      } else {
1274        vec![]
1275      };
1276
1277      // TODO: https://github.com/serai-dex/serai/issues/104
1278      outs
1279        .iter()
1280        .enumerate()
1281        .map(|(i, out)| {
1282          // Allow keys to be invalid, though if they are, return None to trigger selection of a
1283          // new decoy
1284          // Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet
1285          // invalid keys may honestly exist on the blockchain
1286          let Some(key) = out.key.decompress() else {
1287            return Ok(None);
1288          };
1289          Ok(Some([key, out.commitment]).filter(|_| {
1290            if fingerprintable_deterministic {
1291              // https://github.com/monero-project/monero/blob
1292              //   /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core
1293              //   /blockchain.cpp#L90
1294              const ACCEPTED_TIMELOCK_DELTA: usize = 1;
1295
1296              // https://github.com/monero-project/monero/blob
1297              //   /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core
1298              //   /blockchain.cpp#L3836
1299              out.height.checked_add(DEFAULT_LOCK_WINDOW).is_some_and(|locked| locked <= height) &&
1300                (Timelock::Block(height.wrapping_add(ACCEPTED_TIMELOCK_DELTA - 1)) >=
1301                  txs[i].prefix().additional_timelock)
1302            } else {
1303              out.unlocked
1304            }
1305          }))
1306        })
1307        .collect()
1308    }
1309  }
1310}