cycles-quartz/utils/tm-prover/src/main.rs

364 lines
10 KiB
Rust
Raw Normal View History

2023-11-07 13:01:05 +00:00
#![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;
2024-01-11 11:43:12 +00:00
use cw_proof::{
error::ProofError,
proof::{
cw::{CwProof, RawCwProof},
key::CwAbciKey,
Proof,
},
};
2023-11-07 13:01:05 +00:00
use futures::future::join_all;
use serde::{Deserialize, Serialize};
2023-11-07 13:01:05 +00:00
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};
2023-11-07 13:01:05 +00:00
use tracing::{error, info, metadata::LevelFilter};
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};
const WASM_STORE_KEY: &str = "/store/wasm/key";
2023-11-07 13:01:05 +00:00
fn parse_trust_threshold(s: &str) -> Result<TrustThreshold> {
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<T>(Vec<T>);
impl<E, T: FromStr<Err = E>> FromStr for List<T> {
type Err = E;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.split(',')
.map(|s| s.parse())
.collect::<Result<Vec<_>, _>>()
.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<LightBlock>,
merkle_proof: RawCwProof,
}
2023-11-07 13:01:05 +00:00
#[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")]
2023-11-07 13:01:05 +00:00
primary: HttpClientUrl,
/// Comma-separated list of witnesses RPC addresses
#[clap(long)]
witnesses: List<HttpClientUrl>,
/// 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<PathBuf>,
/// 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,
2023-11-07 13:01:05 +00:00
}
#[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(),
2023-11-07 13:01:05 +00:00
args.trusted_height,
args.trusted_hash,
options,
)
.await?;
let client = HttpClient::builder(args.primary.clone()).build()?;
2023-11-07 13:01:05 +00:00
let trusted_block = primary
.latest_trusted()
.ok_or_else(|| eyre!("No trusted state found for primary"))?;
2024-01-04 11:26:51 +00:00
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");
2024-01-04 11:26:51 +00:00
info!("Verifying to latest height on primary...");
2024-01-04 11:26:51 +00:00
let primary_block = primary.verify_to_height(latest_height)?;
2023-11-07 13:01:05 +00:00
info!("Verified to height {} on primary", primary_block.height());
2024-01-03 16:40:29 +00:00
let mut primary_trace = primary.get_trace(primary_block.height());
2023-11-07 13:01:05 +00:00
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::<Result<Vec<_>>>()?;
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?;
2024-01-04 11:26:51 +00:00
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))?;
2023-11-07 13:01:05 +00:00
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
2024-01-03 16:40:29 +00:00
let latest_block = primary.fetch_light_block(status.sync_info.latest_block_height)?;
let _ = primary_trace.pop();
2024-01-03 16:40:29 +00:00
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?;
2023-11-07 13:01:05 +00:00
};
Ok(())
}
async fn write_proof_to_file(trace_file: PathBuf, output: ProofOutput) -> Result<()> {
2023-11-07 13:01:05 +00:00
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<LightBlock>,
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::<Sha256>(
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<Provider> {
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))
}