Tendermint host prover (#12)
This commit is contained in:
commit
7c66a89ef4
5 changed files with 2592 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
*~
|
||||
*.manifest
|
||||
*.manifest.sgx
|
||||
.idea/
|
||||
enclaves/tm/target/
|
||||
utils/tm-prover/target/
|
||||
|
|
2247
utils/tm-prover/Cargo.lock
generated
Normal file
2247
utils/tm-prover/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
utils/tm-prover/Cargo.toml
Normal file
20
utils/tm-prover/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "tm-prover"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tendermint = "0.34.0"
|
||||
tendermint-rpc = { version = "0.34.0", features = ["http-client"] }
|
||||
tendermint-light-client = "0.34.0"
|
||||
tendermint-light-client-detector = "0.34.0"
|
||||
|
||||
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"] }
|
16
utils/tm-prover/README.md
Normal file
16
utils/tm-prover/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# The Tendermint light client prover
|
||||
|
||||
Enables stateless light client verification by generating a light client proof (AKA verification trace) for a given
|
||||
block height and trusted height/hash.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cargo run -- --chain-id osmosis-1 \
|
||||
--primary https://rpc.osmosis.zone \
|
||||
--witnesses https://rpc.osmosis.zone \
|
||||
--trusted-height 12230413 \
|
||||
--trusted-hash D3742DD1573436AF972472135A24B31D5ACE9A2C04791A76196F875955B90F1D \
|
||||
--height 12230423 \
|
||||
--trace-file light-client-proof.json
|
||||
```
|
305
utils/tm-prover/src/main.rs
Normal file
305
utils/tm-prover/src/main.rs
Normal file
|
@ -0,0 +1,305 @@
|
|||
#![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 futures::future::join_all;
|
||||
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, HttpClientUrl};
|
||||
use tracing::{error, info, metadata::LevelFilter};
|
||||
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,
|
||||
|
||||
/// Output file to store light client proof (AKA verification trace)
|
||||
#[clap(long)]
|
||||
trace_file: Option<PathBuf>,
|
||||
|
||||
/// 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);
|
||||
|
||||
run_detector(
|
||||
&mut primary,
|
||||
witnesses.as_mut_slice(),
|
||||
primary_trace.clone(),
|
||||
max_clock_drift,
|
||||
max_block_lag,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(trace_file) = args.trace_file {
|
||||
write_trace_to_file(trace_file, primary_trace).await?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_trace_to_file(trace_file: PathBuf, output: Vec<LightBlock>) -> 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<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))
|
||||
}
|
Loading…
Reference in a new issue