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