#![deny( warnings, trivial_casts, trivial_numeric_casts, unused_import_braces, unused_qualifications )] #![forbid(unsafe_code)] use std::{ fs::File, io::{BufWriter, Write}, path::PathBuf, str::FromStr, time::Duration, }; use clap::Parser; use color_eyre::{ eyre::{eyre, Result}, Report, }; use cosmrs::AccountId; use cw_proof::{ error::ProofError, proof::{ cw::{CwProof, RawCwProof}, key::CwAbciKey, Proof, }, }; use futures::future::join_all; use serde::{Deserialize, Serialize}; use tendermint::{crypto::default::Sha256, evidence::Evidence}; use tendermint_light_client::{ builder::LightClientBuilder, light_client::Options, store::memory::MemoryStore, types::{Hash, Height, LightBlock, TrustThreshold}, }; use tendermint_light_client_detector::{detect_divergence, Error, Provider, Trace}; use tendermint_rpc::{client::HttpClient, Client, HttpClientUrl}; use tracing::{error, info, metadata::LevelFilter}; use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; const WASM_STORE_KEY: &str = "/store/wasm/key"; fn parse_trust_threshold(s: &str) -> Result { if let Some((l, r)) = s.split_once('/') { TrustThreshold::new(l.parse()?, r.parse()?).map_err(Into::into) } else { Err(eyre!( "invalid trust threshold: {s}, format must be X/Y where X and Y are integers" )) } } #[derive(Clone, Debug)] struct List(Vec); impl> FromStr for List { type Err = E; fn from_str(s: &str) -> Result { s.split(',') .map(|s| s.parse()) .collect::, _>>() .map(Self) } } #[derive(clap::Args, Debug, Clone)] struct Verbosity { /// Increase verbosity, can be repeated up to 2 times #[arg(long, short, action = clap::ArgAction::Count)] verbose: u8, } impl Verbosity { fn to_level_filter(&self) -> LevelFilter { match self.verbose { 0 => LevelFilter::INFO, 1 => LevelFilter::DEBUG, _ => LevelFilter::TRACE, } } } #[derive(Clone, Debug, Serialize, Deserialize)] struct ProofOutput { light_client_proof: Vec, merkle_proof: RawCwProof, } #[derive(Debug, Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// Identifier of the chain #[clap(long)] chain_id: String, /// Primary RPC address #[clap(long, default_value = "http://127.0.0.1:26657")] primary: HttpClientUrl, /// Comma-separated list of witnesses RPC addresses #[clap(long)] witnesses: List, /// Height of trusted header #[clap(long)] trusted_height: Height, /// Hash of trusted header #[clap(long)] trusted_hash: Hash, /// Trust threshold #[clap(long, value_parser = parse_trust_threshold, default_value_t = TrustThreshold::TWO_THIRDS)] trust_threshold: TrustThreshold, /// Trusting period, in seconds (default: two weeks) #[clap(long, default_value = "1209600")] trusting_period: u64, /// Maximum clock drift, in seconds #[clap(long, default_value = "5")] max_clock_drift: u64, /// Maximum block lag, in seconds #[clap(long, default_value = "5")] max_block_lag: u64, /// Output file to store light client proof (AKA verification trace) #[clap(long)] trace_file: Option, /// Increase verbosity #[clap(flatten)] verbose: Verbosity, /// Address of the CosmWasm contract #[clap(long)] contract_address: AccountId, /// Storage key of the state item for which proofs must be retrieved #[clap(long)] storage_key: String, } #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; let args = Cli::parse(); let env_filter = EnvFilter::builder() .with_default_directive(args.verbose.to_level_filter().into()) .from_env_lossy(); tracing_subscriber::fmt() .with_target(false) .with_env_filter(env_filter) .finish() .init(); let options = Options { trust_threshold: args.trust_threshold, trusting_period: Duration::from_secs(args.trusting_period), clock_drift: Duration::from_secs(args.max_clock_drift), }; let mut primary = make_provider( &args.chain_id, args.primary.clone(), args.trusted_height, args.trusted_hash, options, ) .await?; let client = HttpClient::builder(args.primary.clone()).build()?; let trusted_block = primary .latest_trusted() .ok_or_else(|| eyre!("No trusted state found for primary"))?; let status = client.status().await?; let latest_height = status.sync_info.latest_block_height; // `proof_height` is the height at which we want to query the blockchain's state // This is one less than than the `latest_height` because we want to verify the merkle-proof for // the state against the `app_hash` at `latest_height`. // (because Tendermint commits to the latest `app_hash` in the subsequent block) let proof_height = (latest_height.value() - 1) .try_into() .expect("infallible conversion"); info!("Verifying to latest height on primary..."); let primary_block = primary.verify_to_height(latest_height)?; info!("Verified to height {} on primary", primary_block.height()); let mut primary_trace = primary.get_trace(primary_block.height()); let witnesses = join_all(args.witnesses.0.into_iter().map(|addr| { make_provider( &args.chain_id, addr, trusted_block.height(), trusted_block.signed_header.header.hash(), options, ) })) .await; let mut witnesses = witnesses.into_iter().collect::>>()?; let max_clock_drift = Duration::from_secs(args.max_clock_drift); let max_block_lag = Duration::from_secs(args.max_block_lag); run_detector( &mut primary, witnesses.as_mut_slice(), primary_trace.clone(), max_clock_drift, max_block_lag, ) .await?; let status = client.status().await?; let latest_app_hash = status.sync_info.latest_app_hash; let path = WASM_STORE_KEY.to_owned(); let data = CwAbciKey::new(args.contract_address, args.storage_key, None); let result = client .abci_query(Some(path), data, Some(proof_height), true) .await?; let proof: CwProof = result .clone() .try_into() .expect("result should contain proof"); proof .verify(latest_app_hash.clone().into()) .map_err(|e: ProofError| eyre!(e))?; if let Some(trace_file) = args.trace_file { // replace the last block in the trace (i.e. the (latest - 1) block) with the latest block // we don't actually verify the latest block because it will be verified on the other side let latest_block = primary.fetch_light_block(status.sync_info.latest_block_height)?; let _ = primary_trace.pop(); primary_trace.push(latest_block); let output = ProofOutput { light_client_proof: primary_trace, merkle_proof: proof.into(), }; write_proof_to_file(trace_file, output).await?; }; Ok(()) } async fn write_proof_to_file(trace_file: PathBuf, output: ProofOutput) -> Result<()> { info!("Writing proof to output file ({})", trace_file.display()); let file = File::create(trace_file)?; let mut writer = BufWriter::new(file); serde_json::to_writer(&mut writer, &output)?; writer.flush()?; Ok(()) } async fn run_detector( primary: &mut Provider, witnesses: &mut [Provider], primary_trace: Vec, max_clock_drift: Duration, max_block_lag: Duration, ) -> Result<(), Report> { if witnesses.is_empty() { return Err(Error::no_witnesses().into()); } info!( "Running misbehavior detection against {} witnesses...", witnesses.len() ); let primary_trace = Trace::new(primary_trace)?; for witness in witnesses { let divergence = detect_divergence::( Some(primary), witness, primary_trace.clone().into_vec(), max_clock_drift, max_block_lag, ) .await; let evidence = match divergence { Ok(Some(divergence)) => divergence.evidence, Ok(None) => { info!( "no divergence found between primary and witness {}", witness.peer_id() ); continue; } Err(e) => { error!( "failed to run attack detector against witness {}: {e}", witness.peer_id() ); continue; } }; // Report the evidence to the witness witness .report_evidence(Evidence::from(evidence.against_primary)) .await .map_err(|e| eyre!("failed to report evidence to witness: {}", e))?; if let Some(against_witness) = evidence.against_witness { // Report the evidence to the primary primary .report_evidence(Evidence::from(against_witness)) .await .map_err(|e| eyre!("failed to report evidence to primary: {}", e))?; } } Ok(()) } async fn make_provider( chain_id: &str, rpc_addr: HttpClientUrl, trusted_height: Height, trusted_hash: Hash, options: Options, ) -> Result { use tendermint_rpc::client::CompatMode; let rpc_client = HttpClient::builder(rpc_addr) .compat_mode(CompatMode::V0_34) .build()?; let node_id = rpc_client.status().await?.node_info.id; let light_store = Box::new(MemoryStore::new()); let instance = LightClientBuilder::prod(node_id, rpc_client.clone(), light_store, options, None) .trust_primary_at(trusted_height, trusted_hash)? .build(); Ok(Provider::new(chain_id.to_string(), instance, rpc_client)) }