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