1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]

use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};

use serde::Deserialize;
use serde_json::json;

use monero_serai::{
  io::decompress_point,
  primitives::Commitment,
  ringct::{RctPrunable, bulletproofs::BatchVerifier},
  transaction::{Input, Transaction},
  block::Block,
};

use monero_rpc::{RpcError, Rpc};
use monero_simple_request_rpc::SimpleRequestRpc;

use tokio::task::JoinHandle;

async fn check_block(rpc: impl Rpc, block_i: usize) {
  let hash = loop {
    match rpc.get_block_hash(block_i).await {
      Ok(hash) => break hash,
      Err(RpcError::ConnectionError(e)) => {
        println!("get_block_hash ConnectionError: {e}");
        continue;
      }
      Err(e) => panic!("couldn't get block {block_i}'s hash: {e:?}"),
    }
  };

  // TODO: Grab the JSON to also check it was deserialized correctly
  #[derive(Deserialize, Debug)]
  struct BlockResponse {
    blob: String,
  }
  let res: BlockResponse = loop {
    match rpc.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await {
      Ok(res) => break res,
      Err(RpcError::ConnectionError(e)) => {
        println!("get_block ConnectionError: {e}");
        continue;
      }
      Err(e) => panic!("couldn't get block {block_i} via block.hash(): {e:?}"),
    }
  };

  let blob = hex::decode(res.blob).expect("node returned non-hex block");
  let block = Block::read(&mut blob.as_slice())
    .unwrap_or_else(|e| panic!("couldn't deserialize block {block_i}: {e}"));
  assert_eq!(block.hash(), hash, "hash differs");
  assert_eq!(block.serialize(), blob, "serialization differs");

  let txs_len = 1 + block.transactions.len();

  if !block.transactions.is_empty() {
    // Test getting pruned transactions
    loop {
      match rpc.get_pruned_transactions(&block.transactions).await {
        Ok(_) => break,
        Err(RpcError::ConnectionError(e)) => {
          println!("get_pruned_transactions ConnectionError: {e}");
          continue;
        }
        Err(e) => panic!("couldn't call get_pruned_transactions: {e:?}"),
      }
    }

    let txs = loop {
      match rpc.get_transactions(&block.transactions).await {
        Ok(txs) => break txs,
        Err(RpcError::ConnectionError(e)) => {
          println!("get_transactions ConnectionError: {e}");
          continue;
        }
        Err(e) => panic!("couldn't call get_transactions: {e:?}"),
      }
    };

    let mut batch = BatchVerifier::new();
    for tx in txs {
      match tx {
        Transaction::V1 { prefix: _, signatures } => {
          assert!(!signatures.is_empty());
          continue;
        }
        Transaction::V2 { prefix: _, proofs: None } => {
          panic!("proofs were empty in non-miner v2 transaction");
        }
        Transaction::V2 { ref prefix, proofs: Some(ref proofs) } => {
          let sig_hash = tx.signature_hash().expect("no signature hash for TX with proofs");
          // Verify all proofs we support proving for
          // This is due to having debug_asserts calling verify within their proving, and CLSAG
          // multisig explicitly calling verify as part of its signing process
          // Accordingly, making sure our signature_hash algorithm is correct is great, and further
          // making sure the verification functions are valid is appreciated
          match &proofs.prunable {
            RctPrunable::AggregateMlsagBorromean { .. } | RctPrunable::MlsagBorromean { .. } => {}
            RctPrunable::MlsagBulletproofs { bulletproof, .. } |
            RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. } => {
              assert!(bulletproof.batch_verify(
                &mut rand_core::OsRng,
                &mut batch,
                &proofs.base.commitments
              ));
            }
            RctPrunable::Clsag { bulletproof, clsags, pseudo_outs } => {
              assert!(bulletproof.batch_verify(
                &mut rand_core::OsRng,
                &mut batch,
                &proofs.base.commitments
              ));

              for (i, clsag) in clsags.iter().enumerate() {
                let (amount, key_offsets, image) = match &prefix.inputs[i] {
                  Input::Gen(_) => panic!("Input::Gen"),
                  Input::ToKey { amount, key_offsets, key_image } => {
                    (amount, key_offsets, key_image)
                  }
                };

                let mut running_sum = 0;
                let mut actual_indexes = vec![];
                for offset in key_offsets {
                  running_sum += offset;
                  actual_indexes.push(running_sum);
                }

                async fn get_outs(
                  rpc: &impl Rpc,
                  amount: u64,
                  indexes: &[u64],
                ) -> Vec<[EdwardsPoint; 2]> {
                  #[derive(Deserialize, Debug)]
                  struct Out {
                    key: String,
                    mask: String,
                  }

                  #[derive(Deserialize, Debug)]
                  struct Outs {
                    outs: Vec<Out>,
                  }

                  let outs: Outs = loop {
                    match rpc
                      .rpc_call(
                        "get_outs",
                        Some(json!({
                          "get_txid": true,
                          "outputs": indexes.iter().map(|o| json!({
                            "amount": amount,
                            "index": o
                          })).collect::<Vec<_>>()
                        })),
                      )
                      .await
                    {
                      Ok(outs) => break outs,
                      Err(RpcError::ConnectionError(e)) => {
                        println!("get_outs ConnectionError: {e}");
                        continue;
                      }
                      Err(e) => panic!("couldn't connect to RPC to get outs: {e:?}"),
                    }
                  };

                  let rpc_point = |point: &str| {
                    decompress_point(
                      hex::decode(point)
                        .expect("invalid hex for ring member")
                        .try_into()
                        .expect("invalid point len for ring member"),
                    )
                    .expect("invalid point for ring member")
                  };

                  outs
                    .outs
                    .iter()
                    .map(|out| {
                      let mask = rpc_point(&out.mask);
                      if amount != 0 {
                        assert_eq!(mask, Commitment::new(Scalar::from(1u8), amount).calculate());
                      }
                      [rpc_point(&out.key), mask]
                    })
                    .collect()
                }

                clsag
                  .verify(
                    &get_outs(&rpc, amount.unwrap_or(0), &actual_indexes).await,
                    image,
                    &pseudo_outs[i],
                    &sig_hash,
                  )
                  .unwrap();
              }
            }
          }
        }
      }
    }
    assert!(batch.verify());
  }

  println!("Deserialized, hashed, and reserialized {block_i} with {txs_len} TXs");
}

#[tokio::main]
async fn main() {
  let args = std::env::args().collect::<Vec<String>>();

  // Read start block as the first arg
  let mut block_i =
    args.get(1).expect("no start block specified").parse::<usize>().expect("invalid start block");

  // How many blocks to work on at once
  let async_parallelism: usize =
    args.get(2).unwrap_or(&"8".to_string()).parse::<usize>().expect("invalid parallelism argument");

  // Read further args as RPC URLs
  let default_nodes = vec![
    "http://xmr-node-uk.cakewallet.com:18081".to_string(),
    "http://xmr-node-eu.cakewallet.com:18081".to_string(),
  ];
  let mut specified_nodes = vec![];
  {
    let mut i = 0;
    loop {
      let Some(node) = args.get(3 + i) else { break };
      specified_nodes.push(node.clone());
      i += 1;
    }
  }
  let nodes = if specified_nodes.is_empty() { default_nodes } else { specified_nodes };

  let rpc = |url: String| async move {
    SimpleRequestRpc::new(url.clone())
      .await
      .unwrap_or_else(|_| panic!("couldn't create SimpleRequestRpc connected to {url}"))
  };
  let main_rpc = rpc(nodes[0].clone()).await;
  let mut rpcs = vec![];
  for i in 0 .. async_parallelism {
    rpcs.push(rpc(nodes[i % nodes.len()].clone()).await);
  }

  let mut rpc_i = 0;
  let mut handles: Vec<JoinHandle<()>> = vec![];
  let mut height = 0;
  loop {
    let new_height = main_rpc.get_height().await.expect("couldn't call get_height");
    if new_height == height {
      break;
    }
    height = new_height;

    while block_i < height {
      if handles.len() >= async_parallelism {
        // Guarantee one handle is complete
        handles.swap_remove(0).await.unwrap();

        // Remove all of the finished handles
        let mut i = 0;
        while i < handles.len() {
          if handles[i].is_finished() {
            handles.swap_remove(i).await.unwrap();
            continue;
          }
          i += 1;
        }
      }

      handles.push(tokio::spawn(check_block(rpcs[rpc_i].clone(), block_i)));
      rpc_i = (rpc_i + 1) % rpcs.len();
      block_i += 1;
    }
  }
}