#![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, } 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::>() })), ) .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::>(); // Read start block as the first arg let mut block_i = args.get(1).expect("no start block specified").parse::().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::().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> = 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; } } }