Merge branch 'master' into hu55a1n1/4-tee-tm-light-client
This commit is contained in:
commit
454fe6a33e
5 changed files with 2589 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
||||||
*.manifest.sgx
|
*.manifest.sgx
|
||||||
.idea/
|
.idea/
|
||||||
enclaves/tm/target/
|
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