Skip to content

Commit

Permalink
light-client: Add CLI for verifying headers, detecting and reporting …
Browse files Browse the repository at this point in the history
…light client attacks (#1308)

* Add CLI for the light client

* Remove `Supervisor` and `EvidenceReporter`, as well as associated examples and tests
  • Loading branch information
romac committed Apr 28, 2023
1 parent de10198 commit 2238d4b
Show file tree
Hide file tree
Showing 13 changed files with 337 additions and 1,764 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"light-client",
"light-client-verifier",
"light-client-detector",
"light-client-cli",
"light-client-js",
"p2p",
"pbt-gen",
Expand Down
37 changes: 37 additions & 0 deletions light-client-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "tendermint-light-client-cli"
version = "0.31.1"
edition = "2021"
license = "Apache-2.0"
readme = "README.md"
keywords = ["blockchain", "bft", "consensus", "cosmos", "tendermint"]
categories = ["cryptography::cryptocurrencies", "network-programming"]
repository = "https://github.com/informalsystems/tendermint-rs"
authors = [
"Informal Systems <hello@informal.systems>",
]

description = """
Implementation of the Tendermint Light Client CLI.
"""

# docs.rs-specific configuration
[package.metadata.docs.rs]
# document all features
all-features = true
# defines the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
tendermint = { version = "0.31.1", path = "../tendermint" }
tendermint-rpc = { version = "0.31.1", path = "../rpc", features = ["http-client"] }
tendermint-light-client = { version = "0.31.1", path = "../light-client" }
tendermint-light-client-detector = { version = "0.31.1", path = "../light-client-detector" }

clap = { version = "4.1.8", features = ["derive"] }
color-eyre = "0.6.2"
futures = "0.3.27"
serde_json = "1.0.94"
tokio = { version = "1.26.0", features = ["full"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
284 changes: 284 additions & 0 deletions light-client-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
#![allow(unused)]

use std::{convert::Infallible, str::FromStr, time::Duration};

use clap::Parser;
use color_eyre::{
eyre::{eyre, Result},
Report,
};
use futures::future::join_all;
use tendermint::{crypto::default::Sha256, evidence::Evidence, Time};
use tendermint_light_client::{
builder::LightClientBuilder,
instance::Instance,
light_client::Options,
store::memory::MemoryStore,
types::{Hash, Height, LightBlock, TrustThreshold},
};
use tendermint_light_client_detector::{
compare_new_header_with_witness, detect_divergence, gather_evidence_from_conflicting_headers,
CompareError, Error, ErrorDetail, Provider, Trace,
};
use tendermint_rpc::{Client, HttpClient, HttpClientUrl, Url};
use tracing::{debug, error, info, metadata::LevelFilter, warn};
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};

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(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)]
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,

/// Height of the header to verify
#[clap(long)]
height: Option<Height>,

/// 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,

/// Increase verbosity
#[clap(flatten)]
verbose: Verbosity,
}

#[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,
args.trusted_height,
args.trusted_hash,
options,
)
.await?;

let trusted_block = primary
.latest_trusted()
.ok_or_else(|| eyre!("No trusted state found for primary"))?;

let primary_block = if let Some(target_height) = args.height {
info!("Verifying to height {} on primary...", target_height);
primary.verify_to_height(target_height)
} else {
info!("Verifying to latest height on primary...");
primary.verify_to_highest()
}?;

info!("Verified to height {} on primary", primary_block.height());
let 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::<Result<Vec<_>>>()?;

let max_clock_drift = Duration::from_secs(args.max_clock_drift);
let max_block_lag = Duration::from_secs(args.max_block_lag);
let now = Time::now();

run_detector(
&mut primary,
witnesses.as_mut_slice(),
primary_trace,
max_clock_drift,
max_block_lag,
now,
)
.await?;

Ok(())
}

async fn run_detector(
primary: &mut Provider,
witnesses: &mut [Provider],
primary_trace: Vec<LightBlock>,
max_clock_drift: Duration,
max_block_lag: Duration,
now: Time,
) -> 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)?;

let last_verified_block = primary_trace.last();
let last_verified_header = &last_verified_block.signed_header;

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))
}
3 changes: 0 additions & 3 deletions light-client/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,4 @@
mod light_client;
pub use light_client::LightClientBuilder;

mod supervisor;
pub use supervisor::SupervisorBuilder;

pub mod error;
Loading

0 comments on commit 2238d4b

Please sign in to comment.