diff --git a/.changelog/unreleased/features/1291-light-client-detector.md b/.changelog/unreleased/features/1291-light-client-detector.md new file mode 100644 index 000000000..62e695a80 --- /dev/null +++ b/.changelog/unreleased/features/1291-light-client-detector.md @@ -0,0 +1,3 @@ +- [`tendermint-light-client-detector`] Implement a light client + attack detector, based on its Go version found in Comet + ([\#1291](https://github.com/informalsystems/tendermint-rs/issues/1291)) \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1c8bb483c..79741c2fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "config", "light-client", "light-client-verifier", + "light-client-detector", "light-client-js", "p2p", "pbt-gen", diff --git a/light-client-detector/Cargo.toml b/light-client-detector/Cargo.toml new file mode 100644 index 000000000..43821a6ec --- /dev/null +++ b/light-client-detector/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "tendermint-light-client-detector" +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 ", +] + +description = """ + Implementation of the Tendermint Light Client Attack Detector. +""" + +# 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-proto = { version = "0.31.1", path = "../proto" } +tendermint-light-client = { version = "0.31.1", path = "../light-client" } + +contracts = { version = "0.6.2", default-features = false } +crossbeam-channel = { version = "0.4.2", default-features = false } +derive_more = { version = "0.99.5", default-features = false, features = ["display"] } +futures = { version = "0.3.4", default-features = false } +serde = { version = "1.0.106", default-features = false } +serde_cbor = { version = "0.11.1", default-features = false, features = ["alloc", "std"] } +serde_derive = { version = "1.0.106", default-features = false } +sled = { version = "0.34.3", optional = true, default-features = false } +static_assertions = { version = "1.1.0", default-features = false } +time = { version = "0.3", default-features = false, features = ["std"] } +tokio = { version = "1.0", default-features = false, features = ["rt"], optional = true } +flex-error = { version = "0.4.4", default-features = false } +tracing = { version = "0.1", default-features = false } +serde_json = { version = "1.0.51", default-features = false } + diff --git a/light-client-detector/src/conflict.rs b/light-client-detector/src/conflict.rs new file mode 100644 index 000000000..465a77db4 --- /dev/null +++ b/light-client-detector/src/conflict.rs @@ -0,0 +1,97 @@ +use tendermint::{crypto::Sha256, evidence::LightClientAttackEvidence, merkle::MerkleHash}; +use tendermint_light_client::verifier::types::LightBlock; +use tracing::{error, error_span, warn}; + +use super::{ + error::Error, evidence::make_evidence, examine::examine_conflicting_header_against_trace, + provider::Provider, trace::Trace, +}; + +#[derive(Clone, Debug)] +pub struct GatheredEvidence { + pub witness_trace: Trace, + + pub against_primary: LightClientAttackEvidence, + pub against_witness: Option, +} + +/// Handles the primary style of attack, which is where a primary and witness have +/// two headers of the same height but with different hashes. +/// +/// If a primary provider is available, then we will also attempt to gather evidence against the +/// witness by examining the witness's trace and holding the primary as the source of truth. +pub async fn gather_evidence_from_conflicting_headers( + primary: Option<&Provider>, + witness: &Provider, + primary_trace: &Trace, + challenging_block: &LightBlock, +) -> Result +where + H: Sha256 + MerkleHash + Default, +{ + let _span = + error_span!("gather_evidence_from_conflicting_headers", witness = %witness.peer_id()) + .entered(); + + let (witness_trace, primary_block) = + examine_conflicting_header_against_trace::(primary_trace, challenging_block, witness) + .map_err(|e| { + error!("Error validating witness's divergent header: {e}"); + e + })?; + + warn!("ATTEMPTED ATTACK DETECTED. Gathering evidence against primary by witness..."); + + // We are suspecting that the primary is faulty, hence we hold the witness as the source of truth + // and generate evidence against the primary that we can send to the witness + + let common_block = witness_trace.first(); + let trusted_block = witness_trace.last(); + + let evidence_against_primary = make_evidence( + primary_block.clone(), + trusted_block.clone(), + common_block.clone(), + ); + + if primary_block.signed_header.commit.round != trusted_block.signed_header.commit.round { + error!( + "The light client has detected, and prevented, an attempted amnesia attack. + We think this attack is pretty unlikely, so if you see it, that's interesting to us. + Can you let us know by opening an issue through https://github.com/tendermint/tendermint/issues/new" + ); + } + + let Some(primary) = primary else { + return Ok(GatheredEvidence { + witness_trace, + against_primary: evidence_against_primary, + against_witness: None, + }); + }; + + // This may not be valid because the witness itself is at fault. So now we reverse it, examining the + // trace provided by the witness and holding the primary as the source of truth. Note: primary may not + // respond but this is okay as we will halt anyway. + let (primary_trace, witness_block) = + examine_conflicting_header_against_trace::(&witness_trace, &primary_block, primary) + .map_err(|e| { + error!("Error validating primary's divergent header: {e}"); + e + })?; + + warn!("Gathering evidence against witness by primary..."); + + // We now use the primary trace to create evidence against the witness and send it to the primary + let common_block = primary_trace.first(); + let trusted_block = primary_trace.last(); + + let evidence_against_witness = + make_evidence(witness_block, trusted_block.clone(), common_block.clone()); + + Ok(GatheredEvidence { + witness_trace, + against_primary: evidence_against_primary, + against_witness: Some(evidence_against_witness), + }) +} diff --git a/light-client-detector/src/detect.rs b/light-client-detector/src/detect.rs new file mode 100644 index 000000000..6f4e7c030 --- /dev/null +++ b/light-client-detector/src/detect.rs @@ -0,0 +1,242 @@ +use std::{thread, time::Duration}; + +use tracing::{debug, warn}; + +use tendermint::{block::signed_header::SignedHeader, crypto::Sha256, merkle::MerkleHash}; +use tendermint_light_client::light_client::TargetOrLatest; +use tendermint_light_client::verifier::errors::ErrorExt; +use tendermint_light_client::verifier::types::LightBlock; + +use crate::conflict::GatheredEvidence; + +use super::{ + error::Error, gather_evidence_from_conflicting_headers, provider::Provider, trace::Trace, +}; + +/// A divergence between the primary and a witness that has been detected in [`detect_divergence`]. +#[derive(Clone, Debug)] +pub struct Divergence { + /// The evidence of a misbehaviour that has been gathered from the conflicting headers + pub evidence: GatheredEvidence, + /// The conflicting light block that was returned by the witness + pub challenging_block: LightBlock, +} + +/// Given a primary trace and a witness, detect any divergence between the two, +/// by querying the witness for the same header as the last header in the primary trace +/// (ie. the target block), and comparing the hashes. +/// +/// If the hashes match, then no divergence has been detected and the target block can be trusted. +/// +/// If the hashes do not match, then the witness has provided a conflicting header. +/// This could possibly imply an attack on the light client. +/// In this case, we need to verify the witness's header using the same skipping verification +/// and then we need to find the point that the headers diverge and examine this for any evidence of +/// an attack. We then attempt to find the bifurcation point and if successful construct the +/// evidence of an attack to report to the witness. +pub async fn detect_divergence( + primary: Option<&Provider>, + witness: &mut Provider, + primary_trace: Vec, + max_clock_drift: Duration, + max_block_lag: Duration, +) -> Result, Error> +where + H: Sha256 + MerkleHash + Default, +{ + let primary_trace = Trace::new(primary_trace)?; + + let last_verified_block = primary_trace.last(); + let last_verified_header = &last_verified_block.signed_header; + + debug!( + end_block_height = %last_verified_header.header.height, + end_block_hash = %last_verified_header.header.hash(), + length = primary_trace.len(), + "Running detector against primary trace" + ); + + let result = compare_new_header_with_witness( + last_verified_header, + witness, + max_clock_drift, + max_block_lag, + ); + + match result { + // No divergence found + Ok(()) => Ok(None), + + // We have conflicting headers. This could possibly imply an attack on the light client. + // First we need to verify the witness's header using the same skipping verification and then we + // need to find the point that the headers diverge and examine this for any evidence of an attack. + // + // We combine these actions together, verifying the witnesses headers and outputting the trace + // which captures the bifurcation point and if successful provides the information to create valid evidence. + Err(CompareError::ConflictingHeaders(challenging_block)) => { + warn!( + witness = %witness.peer_id(), + height = %challenging_block.height(), + "Found conflicting headers between primary and witness" + ); + + // Gather the evidence to report from the conflicting headers + let evidence = gather_evidence_from_conflicting_headers::( + primary, + witness, + &primary_trace, + &challenging_block, + ) + .await?; + + Ok(Some(Divergence { + evidence, + challenging_block: *challenging_block, + })) + }, + + Err(CompareError::BadWitness) => { + // These are all melevolent errors and should result in removing the witness + debug!(witness = %witness.peer_id(), "witness returned an error during header comparison, removing..."); + + Err(Error::bad_witness()) + }, + + Err(CompareError::Other(e)) => { + // Benign errors which can be ignored + debug!(witness = %witness.peer_id(), "error in light block request to witness: {e}"); + + Err(Error::light_client(e)) + }, + } +} + +/// An error that arised when comparing a header from the primary with a header from a witness +/// with [`compare_new_header_with_witness`]. +#[derive(Debug)] +pub enum CompareError { + /// There may have been an attack on this light client + ConflictingHeaders(Box), + /// The witness has either not responded, doesn't have the header or has given us an invalid one + BadWitness, + /// Some other error has occurred, this is likely a benign error + Other(tendermint_light_client::errors::Error), +} + +/// Takes the verified header from the primary and compares it with a +/// header from a specified witness. The function can return one of three errors: +/// +/// 1: `CompareError::ConflictingHeaders`: there may have been an attack on this light client +/// 2: `CompareError::BadWitness`: the witness has either not responded, doesn't have the header or has given us an invalid one +/// 3: `CompareError::Other`: some other error has occurred, this is likely a benign error +/// +/// Note: In the case of an invalid header we remove the witness +/// +/// 3: nil -> the hashes of the two headers match +pub fn compare_new_header_with_witness( + new_header: &SignedHeader, + witness: &mut Provider, + max_clock_drift: Duration, + max_block_lag: Duration, +) -> Result<(), CompareError> { + let light_block = check_against_witness(new_header, witness, max_clock_drift, max_block_lag)?; + + if light_block.signed_header.header.hash() != new_header.header.hash() { + return Err(CompareError::ConflictingHeaders(Box::new(light_block))); + } + + Ok(()) +} + +fn check_against_witness( + sh: &SignedHeader, + witness: &mut Provider, + max_clock_drift: Duration, + max_block_lag: Duration, +) -> Result { + let _span = + tracing::debug_span!("check_against_witness", witness = %witness.peer_id()).entered(); + + let light_block = witness.fetch_light_block(sh.header.height); + + match light_block { + // No error means we move on to checking the hash of the two headers + Ok(lb) => Ok(lb), + + // The witness hasn't been helpful in comparing headers, we mark the response and continue + // comparing with the rest of the witnesses + Err(e) if e.detail().is_io() => { + debug!("The witness hasn't been helpful in comparing headers"); + + Err(CompareError::BadWitness) + }, + + // The witness' head of the blockchain is lower than the height of the primary. + // This could be one of two things: + // 1) The witness is lagging behind + // 2) The primary may be performing a lunatic attack with a height and time in the future + Err(e) if e.detail().is_height_too_high() => { + debug!("The witness' head of the blockchain is lower than the height of the primary"); + + let light_block = witness + .get_target_block_or_latest(sh.header.height) + .map_err(|_| CompareError::BadWitness)?; + + let light_block = match light_block { + // If the witness caught up and has returned a block of the target height then we can + // break from this switch case and continue to verify the hashes + TargetOrLatest::Target(light_block) => return Ok(light_block), + + // Otherwise we continue with the checks + TargetOrLatest::Latest(light_block) => light_block, + }; + + // The witness' last header is below the primary's header. + // We check the times to see if the blocks have conflicting times + debug!("The witness' last header is below the primary's header"); + + if !light_block.time().before(sh.header.time) { + return Err(CompareError::ConflictingHeaders(Box::new(light_block))); + } + + // The witness is behind. We wait for a period WAITING = 2 * DRIFT + LAG. + // This should give the witness ample time if it is a participating member + // of consensus to produce a block that has a time that is after the primary's + // block time. If not the witness is too far behind and the light client removes it + let wait_time = 2 * max_clock_drift + max_block_lag; + debug!("The witness is behind. We wait for {wait_time:?}"); + + thread::sleep(wait_time); + + let light_block = witness + .get_target_block_or_latest(sh.header.height) + .map_err(|_| CompareError::BadWitness)?; + + let light_block = match light_block { + // If the witness caught up and has returned a block of the target height then we can + // return and continue to verify the hashes + TargetOrLatest::Target(light_block) => return Ok(light_block), + + // Otherwise we continue with the checks + TargetOrLatest::Latest(light_block) => light_block, + }; + + // The witness still doesn't have a block at the height of the primary. + // Check if there is a conflicting time + if !light_block.time().before(sh.header.time) { + return Err(CompareError::ConflictingHeaders(Box::new(light_block))); + } + + // Following this request response procedure, the witness has been unable to produce a block + // that can somehow conflict with the primary's block. We thus conclude that the witness + // is too far behind and thus we return an error. + // + // NOTE: If the clock drift / lag has been miscalibrated it is feasible that the light client has + // drifted too far ahead for any witness to be able provide a comparable block and thus may allow + // for a malicious primary to attack it + Err(CompareError::BadWitness) + }, + + Err(other) => Err(CompareError::Other(other)), + } +} diff --git a/light-client-detector/src/error.rs b/light-client-detector/src/error.rs new file mode 100644 index 000000000..1cb876cce --- /dev/null +++ b/light-client-detector/src/error.rs @@ -0,0 +1,88 @@ +use tendermint::{block::Height, Hash, Time}; +use tendermint_light_client::components::io::IoError; +use tendermint_light_client::errors::Error as LightClientError; +use tendermint_light_client::verifier::types::LightBlock; + +use crate::conflict::GatheredEvidence; + +flex_error::define_error! { + /// Error type for the light client detector. See [`ErrorDetail`] for all the possible error variants. + /// + /// All the possible error variants. + #[derive(Debug)] + Error { + Io + [ IoError ] + |_| { "I/O error" }, + + LightClient + [ LightClientError ] + |_| { "light client error" }, + + NoDivergence + |_| { "expected divergence between conflicting headers but none found" }, + + Divergence + { + evidence: GatheredEvidence, + challenging_block: LightBlock, + } + |e| { format_args!("divergence detected, found evidence: {:#?}", e.evidence) }, + + NoWitnesses + |_| { "no witnesses provided" }, + + BadWitness + |_| { "bad witness" }, + + TargetBlockLowerThanTrusted + { + target_height: Height, + trusted_height: Height, + } + |e| { + format_args!( + "target block height ({}) lower than trusted block height ({})", + e.target_height, e.trusted_height + ) + }, + + TrustedHashDifferentFromSourceFirstBlock + { + expected_hash: Hash, + got_hash: Hash, + } + |e| { + format_args!( + "trusted block is different to the source's first block. Expected hash: {}, got: {}", + e.expected_hash, e.got_hash + ) + }, + + TraceTooShort + { + trace: Vec, + } + |e| { + format_args!( + "trace is too short. Expected at least 2 blocks, got {} block", + e.trace.len() + ) + }, + + TraceBlockAfterTargetBlock + { + trace_time: Time, + target_time: Time, + } + |e| { + format_args!( + "trace block ({}) is after target block ({})", + e.trace_time, e.target_time + ) + }, + + FailedHeaderCrossReferencing + |_| { format_args!("failed to cross-reference header with witness") }, + } +} diff --git a/light-client-detector/src/evidence.rs b/light-client-detector/src/evidence.rs new file mode 100644 index 000000000..717bc71b0 --- /dev/null +++ b/light-client-detector/src/evidence.rs @@ -0,0 +1,152 @@ +use std::cmp::Ordering; + +use tendermint::{ + block::{signed_header::SignedHeader, Header}, + evidence::{ConflictingBlock, LightClientAttackEvidence}, + validator, +}; + +use tendermint_light_client::verifier::types::LightBlock; + +/// Determines the type of attack and then forms the evidence filling out +/// all the fields such that it is ready to be sent to a full node. +pub fn make_evidence( + conflicted: LightBlock, + trusted: LightBlock, + common: LightBlock, +) -> LightClientAttackEvidence { + let conflicting_header_is_invalid = conflicting_header_is_invalid( + &conflicted.signed_header.header, + &trusted.signed_header.header, + ); + + let conflicting_block = ConflictingBlock { + signed_header: conflicted.signed_header, + validator_set: conflicted.validators, + }; + + let byzantine_validators = get_byzantine_validators( + &conflicting_block, + &common.validators, + &trusted.signed_header, + ); + + let witness = if conflicting_header_is_invalid { + common + } else { + trusted + }; + + LightClientAttackEvidence { + conflicting_block, + byzantine_validators, + common_height: witness.height(), + total_voting_power: witness.validators.total_voting_power(), + timestamp: witness.time(), + } +} + +/// Take a trusted header and match it againt a conflicting header +/// to determine whether the conflicting header was the product of a valid state transition +/// or not. If it is then all the deterministic fields of the header should be the same. +/// If not, it is an invalid header and constitutes a lunatic attack. +fn conflicting_header_is_invalid(conflicted: &Header, trusted: &Header) -> bool { + trusted.validators_hash != conflicted.validators_hash + || trusted.next_validators_hash != conflicted.next_validators_hash + || trusted.consensus_hash != conflicted.consensus_hash + || trusted.app_hash != conflicted.app_hash + || trusted.last_results_hash != conflicted.last_results_hash +} + +/// Find out what style of attack `LightClientAttackEvidence` was and then works out who +/// the malicious validators were and returns them. This is used both for forming the `byzantine_validators` +/// field and for validating that it is correct. Validators are ordered based on validator power. +fn get_byzantine_validators( + conflicted: &ConflictingBlock, + common_validators: &validator::Set, + trusted: &SignedHeader, +) -> Vec { + // First check if the header is invalid. This means that it is a lunatic attack and therefore we take the + // validators who are in the `common_validators` and voted for the lunatic header + if conflicting_header_is_invalid(&conflicted.signed_header.header, &trusted.header) { + find_lunatic_validators(conflicted, common_validators) + } else if trusted.commit.round == conflicted.signed_header.commit.round { + // This is an equivocation attack as both commits are in the same round. We then find the validators + // from the conflicting light block validator set that voted in both headers. + // Validator hashes are the same therefore the indexing order of validators are the same and thus we + // only need a single loop to find the validators that voted twice. + + find_equivocating_validators(conflicted, trusted) + } else { + // if the rounds are different then this is an amnesia attack. Unfortunately, given the nature of the attack, + // we aren't able yet to deduce which are malicious validators and which are not hence we return an + // empty validator set. + + Vec::new() + } +} + +fn find_lunatic_validators( + conflicted: &ConflictingBlock, + common_validators: &validator::Set, +) -> Vec { + let mut validators = Vec::new(); + + for commit_sig in &conflicted.signed_header.commit.signatures { + if !commit_sig.is_commit() { + continue; + } + + let validator = commit_sig + .validator_address() + .and_then(|addr| common_validators.validator(addr)); + + if let Some(validator) = validator { + validators.push(validator); + } + } + + validators.sort_by(cmp_voting_power_then_address); + validators +} + +fn find_equivocating_validators( + conflicted: &ConflictingBlock, + trusted: &SignedHeader, +) -> Vec { + let mut validators = Vec::new(); + + for (i, sig_a) in conflicted + .signed_header + .commit + .signatures + .iter() + .enumerate() + { + if sig_a.is_absent() { + continue; + } + + let sig_b = &trusted.commit.signatures[i]; + if sig_b.is_nil() { + continue; + } + + let validator = sig_a + .validator_address() + .and_then(|addr| conflicted.validator_set.validator(addr)); + + if let Some(validator) = validator { + validators.push(validator); + } + } + + validators.sort_by(cmp_voting_power_then_address); + validators +} + +fn cmp_voting_power_then_address(a: &validator::Info, b: &validator::Info) -> Ordering { + a.power + .cmp(&b.power) + .then_with(|| a.address.cmp(&b.address)) +} diff --git a/light-client-detector/src/examine.rs b/light-client-detector/src/examine.rs new file mode 100644 index 000000000..84cddbdfd --- /dev/null +++ b/light-client-detector/src/examine.rs @@ -0,0 +1,210 @@ +use tendermint::{crypto::Sha256, merkle::MerkleHash}; +use tendermint_light_client::{ + state::State, + store::{memory::MemoryStore, LightStore}, + verifier::types::{LightBlock, Status}, +}; + +use super::{error::Error, provider::Provider, trace::Trace}; + +// examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that +// it has received from another and preforms verifySkipping at the heights of each of the intermediate +// headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen. +// +// 1. The light client verifies a header that is different to the intermediate header in the trace. This +// is the bifurcation point and the light client can create evidence from it +// 2. The source stops responding, doesn't have the block or sends an invalid header in which case we +// return the error and remove the witness +// +// CONTRACT: +// 1. Trace can not be empty len(trace) > 0 +// 2. The last block in the trace can not be of a lower height than the target block +// trace[len(trace)-1].Height >= targetBlock.Height +// 3. The last block in the trace is conflicting with the target block +pub fn examine_conflicting_header_against_trace( + trace: &Trace, + target_block: &LightBlock, + source: &Provider, +) -> Result<(Trace, LightBlock), Error> +where + H: Sha256 + MerkleHash + Default, +{ + let trusted_block = trace.first(); + + if target_block.height() < trusted_block.height() { + return Err(Error::target_block_lower_than_trusted( + target_block.height(), + trusted_block.height(), + )); + }; + + let mut previously_verified_block = + check_trusted_block::(source, trusted_block, target_block)?; + + for trace_block in trace.iter().skip(1) { + let result = examine_conflicting_header_against_trace_block::( + source, + trace_block, + target_block, + previously_verified_block, + )?; + + match result { + ExaminationResult::Continue(prev_verified_block) => { + previously_verified_block = prev_verified_block; + continue; + }, + ExaminationResult::Divergence(source_trace, trace_block) => { + return Ok((source_trace, trace_block)); + }, + } + } + + // We have reached the end of the trace. This should never happen. This can only happen if one of the stated + // prerequisites to this function were not met. + // Namely that either trace[len(trace)-1].Height < targetBlock.Height + // or that trace[i].Hash() != targetBlock.Hash() + Err(Error::no_divergence()) +} + +#[derive(Debug)] +pub enum ExaminationResult { + Continue(LightBlock), + Divergence(Trace, LightBlock), +} + +fn check_trusted_block( + source: &Provider, + trusted_block: &LightBlock, + target_block: &LightBlock, +) -> Result +where + H: Sha256 + MerkleHash + Default, +{ + // This case only happens in a forward lunatic attack. We treat the block with the + // height directly after the targetBlock as the divergent block + if trusted_block.height() > target_block.height() { + // sanity check that the time of the traceBlock is indeed less than that of the targetBlock. If the trace + // was correctly verified we should expect monotonically increasing time. This means that if the block at + // the end of the trace has a lesser time than the target block then all blocks in the trace should have a + // lesser time + if trusted_block.time() > target_block.time() { + return Err(Error::trace_block_after_target_block( + trusted_block.time(), + target_block.time(), + )); + } + } + + // get the corresponding block from the source to verify and match up against the traceBlock + let source_block = if trusted_block.height() == target_block.height() { + target_block.clone() + } else { + source + .fetch_light_block(trusted_block.height()) + .map_err(Error::light_client)? + }; + + let source_block_hash = source_block.signed_header.header.hash_with::(); + let trace_block_hash = trusted_block.signed_header.header.hash_with::(); + + // the first block in the trace MUST be the same to the light block that the source produces + // else we cannot continue with verification. + if source_block_hash != trace_block_hash { + Err(Error::trusted_hash_different_from_source_first_block( + source_block_hash, + trace_block_hash, + )) + } else { + Ok(source_block) + } +} + +// check of primary is same as witness block at that height + +fn examine_conflicting_header_against_trace_block( + source: &Provider, + trace_block: &LightBlock, + target_block: &LightBlock, + prev_verified_block: LightBlock, +) -> Result +where + H: Sha256 + MerkleHash + Default, +{ + // This case only happens in a forward lunatic attack. We treat the block with the + // height directly after the targetBlock as the divergent block + if trace_block.height() > target_block.height() { + // sanity check that the time of the traceBlock is indeed less than that of the targetBlock. If the trace + // was correctly verified we should expect monotonically increasing time. This means that if the block at + // the end of the trace has a lesser time than the target block then all blocks in the trace should have a + // lesser time + if trace_block.time() > target_block.time() { + return Err(Error::trace_block_after_target_block( + trace_block.time(), + target_block.time(), + )); + } + + // Before sending back the divergent block and trace we need to ensure we have verified + // the final gap between the previouslyVerifiedBlock and the targetBlock + if prev_verified_block.height() != target_block.height() { + let source_trace = verify_skipping(source, prev_verified_block, target_block.clone())?; + + return Ok(ExaminationResult::Divergence( + source_trace, + trace_block.clone(), + )); + } + } + + // get the corresponding block from the source to verify and match up against the traceBlock + let source_block = if trace_block.height() == target_block.height() { + target_block.clone() + } else { + source + .fetch_light_block(trace_block.height()) + .map_err(Error::light_client)? + }; + + let source_block_hash = source_block.signed_header.header.hash_with::(); + let trace_block_hash = trace_block.signed_header.header.hash_with::(); + + // we check that the source provider can verify a block at the same height of the + // intermediate height + let source_trace = verify_skipping(source, prev_verified_block, source_block.clone())?; + + // check if the headers verified by the source has diverged from the trace + if source_block_hash != trace_block_hash { + // Bifurcation point found! + return Ok(ExaminationResult::Divergence( + source_trace, + trace_block.clone(), + )); + } + + // headers are still the same, continue + Ok(ExaminationResult::Continue(source_block)) +} + +fn verify_skipping( + source: &Provider, + trusted: LightBlock, + target: LightBlock, +) -> Result { + let target_height = target.height(); + + let mut store = MemoryStore::new(); + store.insert(trusted, Status::Trusted); + store.insert(target, Status::Unverified); + + let mut state = State::new(store); + + let _ = source + .verify_to_height_with_state(target_height, &mut state) + .map_err(Error::light_client)?; + + let blocks = state.get_trace(target_height); + let source_trace = Trace::new(blocks)?; + + Ok(source_trace) +} diff --git a/light-client-detector/src/lib.rs b/light-client-detector/src/lib.rs new file mode 100644 index 000000000..2d8216a94 --- /dev/null +++ b/light-client-detector/src/lib.rs @@ -0,0 +1,18 @@ +//! The detector component of the light client detects and handles attacks on the light client. +//! +//! See [`detect_divergence`] for the main entry point. + +mod conflict; +mod detect; +mod error; +mod evidence; +mod examine; +mod provider; +mod trace; + +pub use conflict::gather_evidence_from_conflicting_headers; +pub use detect::{compare_new_header_with_witness, detect_divergence, CompareError, Divergence}; +pub use error::{Error, ErrorDetail}; +pub use provider::Provider; +pub use tendermint::evidence::{Evidence, LightClientAttackEvidence}; +pub use trace::Trace; diff --git a/light-client-detector/src/provider.rs b/light-client-detector/src/provider.rs new file mode 100644 index 000000000..8abb53594 --- /dev/null +++ b/light-client-detector/src/provider.rs @@ -0,0 +1,86 @@ +use tendermint::block::Height; +use tendermint::evidence::Evidence; +use tendermint::hash::Hash; +use tendermint_light_client::errors::Error; +use tendermint_light_client::instance::Instance; +use tendermint_light_client::light_client::TargetOrLatest; +use tendermint_light_client::state::State; +use tendermint_light_client::store::memory::MemoryStore; +use tendermint_light_client::verifier::types::LightBlock; +use tendermint_rpc::{Client, Error as RpcError, HttpClient}; + +/// A interface over a light client instance and its RPC client. +#[derive(Debug)] +pub struct Provider { + chain_id: String, + instance: Instance, + rpc_client: HttpClient, +} + +impl Provider { + pub fn new(chain_id: String, instance: Instance, rpc_client: HttpClient) -> Self { + Self { + chain_id, + instance, + rpc_client, + } + } + + pub fn chain_id(&self) -> &str { + &self.chain_id + } + + pub fn peer_id(&self) -> &tendermint::node::Id { + self.instance.peer_id() + } + + pub async fn report_evidence(&self, evidence: Evidence) -> Result { + self.rpc_client + .broadcast_evidence(evidence) + .await + .map(|response| response.hash) + } + + pub fn fetch_light_block(&self, height: Height) -> Result { + let mut state = State::new(MemoryStore::new()); + + self.instance + .light_client + .get_or_fetch_block(height, &mut state) + .map(|(lb, _)| lb) + } + + pub fn verify_to_highest(&mut self) -> Result { + self.instance + .light_client + .verify_to_highest(&mut self.instance.state) + } + + pub fn verify_to_height(&mut self, height: Height) -> Result { + self.instance + .light_client + .verify_to_target(height, &mut self.instance.state) + } + + pub fn verify_to_height_with_state( + &self, + height: Height, + state: &mut State, + ) -> Result { + self.instance.light_client.verify_to_target(height, state) + } + + pub fn get_target_block_or_latest(&mut self, height: Height) -> Result { + self.instance + .light_client + .get_target_block_or_latest(height, &mut self.instance.state) + } + + pub fn get_trace(&self, height: Height) -> Vec { + self.instance.state.get_trace(height) + } + + pub fn latest_trusted(&self) -> Option { + self.instance.latest_trusted() + } +} diff --git a/light-client-detector/src/trace.rs b/light-client-detector/src/trace.rs new file mode 100644 index 000000000..f528ba0de --- /dev/null +++ b/light-client-detector/src/trace.rs @@ -0,0 +1,55 @@ +use tendermint_light_client::verifier::types::LightBlock; + +use super::Error; + +/// A trace of the light blocks that were used by the light client to verify a particular header, +/// in the case of bisection or sequential verification. +/// +/// The trace always contains at least two light blocks, the trusted block and the target block. +#[derive(Clone, Debug)] +pub struct Trace(Vec); + +impl Trace { + pub fn new(mut trace: Vec) -> Result { + if trace.len() < 2 { + return Err(Error::trace_too_short(trace)); + } + + trace.sort_unstable_by_key(|lb| lb.height()); + + Ok(Self(trace)) + } + + pub fn first(&self) -> &LightBlock { + self.0.first().expect("trace is empty, which cannot happen") + } + + pub fn last(&self) -> &LightBlock { + self.0.last().expect("trace is empty, which cannot happen") + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> std::slice::Iter<'_, LightBlock> { + self.0.iter() + } + + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl IntoIterator for Trace { + type Item = LightBlock; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/light-client-verifier/src/errors.rs b/light-client-verifier/src/errors.rs index 8132bd778..22b928e18 100644 --- a/light-client-verifier/src/errors.rs +++ b/light-client-verifier/src/errors.rs @@ -187,6 +187,13 @@ pub trait ErrorExt { /// Whether this error means that a timeout occurred when /// querying a node. fn is_timeout(&self) -> Option; + + /// Whether an I/O error occured when querying a node. + fn is_io(&self) -> bool; + + /// Whether the height of the requested light block is higher than the + /// latest height available on the node. + fn is_height_too_high(&self) -> bool; } impl ErrorExt for VerificationErrorDetail { @@ -204,4 +211,12 @@ impl ErrorExt for VerificationErrorDetail { fn is_timeout(&self) -> Option { None } + + fn is_io(&self) -> bool { + false + } + + fn is_height_too_high(&self) -> bool { + false + } } diff --git a/light-client-verifier/src/types.rs b/light-client-verifier/src/types.rs index 99ec58480..2942509bb 100644 --- a/light-client-verifier/src/types.rs +++ b/light-client-verifier/src/types.rs @@ -141,6 +141,14 @@ impl LightBlock { self.signed_header.header.height } + /// Returns the time at which this block was created. + /// + /// ## Note + /// This is a shorthand for `block.signed_header.header.time`. + pub fn time(&self) -> Time { + self.signed_header.header.time + } + /// Obtain the verification parameters for the light block when using it as /// trusted state. pub fn as_trusted_state(&self) -> TrustedBlockState<'_> { diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 7ce69f2a0..96dab902a 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -51,6 +51,9 @@ static_assertions = { version = "1.1.0", default-features = false } time = { version = "0.3", default-features = false, features = ["std"] } tokio = { version = "1.0", default-features = false, features = ["rt"], optional = true } flex-error = { version = "0.4.4", default-features = false } +tracing = { version = "0.1", default-features = false } +serde_json = { version = "1.0.51", default-features = false } +regex = { version = "1.7.3" } [dev-dependencies] tendermint-testgen = { path = "../testgen", default-features = false } @@ -60,7 +63,3 @@ gumdrop = { version = "0.8.0", default-features = false } rand = { version = "0.7.3", default-features = false } tempfile = { version = "3.2.0", default-features = false } proptest = { version = "0.10.1", default-features = false, features = ["std"] } - -[[example]] -name = "light_client" -required-features = ["rpc-client", "tendermint-rpc/http-client", "flex-error/std"] diff --git a/light-client/README.md b/light-client/README.md index db98c850d..0658993d8 100644 --- a/light-client/README.md +++ b/light-client/README.md @@ -12,53 +12,6 @@ and [Fork Detection][light-client-detection] protocols. See documentation on [crates.io][docs-link]. -## Example - -The code below demonstrates the main use case for the Tendermint Light Client: syncing to the latest block, verifying it, and performing fork detection. - -Please refer to the [`light_client` example](https://github.com/informalsystems/tendermint-rs/blob/main/light-client/examples/light_client.rs) for fully working code. - -```rust -let primary_instance: Instance = make_instance(primary, primary_addr, primary_path); -let witness_instance: Instance = make_instance(witness, witness_addr, witness_path); - -let mut peer_addr = HashMap::new(); -peer_addr.insert(primary, primary_addr); -peer_addr.insert(witness, witness_addr); - -let peer_list = PeerList::builder() - .primary(primary, primary_instance) - .witness(witness, witness_instance) - .build(); - -let mut supervisor = Supervisor::new( - peer_list, - ProdForkDetector::default(), - ProdEvidenceReporter::new(peer_addr), -); - -let mut handle = supervisor.handle(); - -// Spawn the supervisor in its own thread. -std::thread::spawn(|| supervisor.run()); - -loop { - // Synchronously query the supervisor via a handle - let block = handle.verify_to_highest(); - - match block { - Ok(light_block) => { - println!("[info] synced to block {}", light_block.height()); - } - Err(e) => { - println!("[error] sync failed: {}", e); - } - }); - - std::thread::sleep(Duration::from_millis(800)); -} -``` - ## Testing The Tendermint Light Client is primarily tested through unit tests. diff --git a/light-client/examples/light_client.rs b/light-client/examples/light_client.rs deleted file mode 100644 index 94d83a95f..000000000 --- a/light-client/examples/light_client.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::{path::PathBuf, time::Duration}; - -use gumdrop::Options; -use tendermint::Hash; -use tendermint_light_client::{ - builder::{LightClientBuilder, SupervisorBuilder}, - store::memory::MemoryStore, - supervisor::{Handle as _, Instance}, - verifier::{ - options::Options as LightClientOptions, - types::{Height, PeerId, TrustThreshold}, - }, -}; -use tendermint_rpc as rpc; - -#[derive(Debug, Options)] -struct CliOptions { - #[options(help = "print this help message")] - help: bool, - #[options(help = "enable verbose output")] - verbose: bool, - - #[options(command)] - command: Option, -} - -#[derive(Debug, Options)] -enum Command { - #[options(help = "run the light client and continuously sync up to the latest block")] - Sync(SyncOpts), -} - -#[derive(Debug, Options)] -struct SyncOpts { - #[options(help = "show help for this command")] - help: bool, - #[options( - help = "address of the Tendermint node to connect to", - meta = "ADDR", - default = "tcp://127.0.0.1:26657" - )] - address: tendermint_rpc::Url, - #[options( - help = "height of the initial trusted state (optional if store already initialized)", - meta = "HEIGHT" - )] - trusted_height: Option, - #[options( - help = "hash of the initial trusted state (optional if store already initialized)", - meta = "HASH" - )] - trusted_hash: Option, - #[options( - help = "path to the database folder", - meta = "PATH", - default = "./lightstore" - )] - db_path: PathBuf, -} - -fn main() { - let opts = CliOptions::parse_args_default_or_exit(); - - match opts.command { - None => { - eprintln!("Please specify a command:"); - eprintln!("{}\n", CliOptions::command_list().unwrap()); - eprintln!("{}\n", CliOptions::usage()); - std::process::exit(1); - }, - Some(Command::Sync(sync_opts)) => sync_cmd(sync_opts).unwrap_or_else(|e| { - eprintln!("Command failed: {e}"); - std::process::exit(1); - }), - } -} - -fn make_instance( - peer_id: PeerId, - addr: tendermint_rpc::Url, - opts: &SyncOpts, -) -> Result> { - let light_store = MemoryStore::new(); - let rpc_client = rpc::HttpClient::new(addr).unwrap(); - let options = LightClientOptions { - trust_threshold: TrustThreshold::default(), - trusting_period: Duration::from_secs(36000), - clock_drift: Duration::from_secs(1), - }; - - let builder = - LightClientBuilder::prod(peer_id, rpc_client, Box::new(light_store), options, None); - - let builder = if let (Some(height), Some(hash)) = (opts.trusted_height, opts.trusted_hash) { - builder.trust_primary_at(height, hash) - } else { - builder.trust_from_store() - }?; - - Ok(builder.build()) -} - -fn sync_cmd(opts: SyncOpts) -> Result<(), Box> { - let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(); - let witness: PeerId = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF".parse().unwrap(); - - let primary_addr = opts.address.clone(); - let witness_addr = opts.address.clone(); - - let primary_instance = make_instance(primary, primary_addr.clone(), &opts)?; - let witness_instance = make_instance(witness, witness_addr.clone(), &opts)?; - - let supervisor = SupervisorBuilder::new() - .primary(primary, primary_addr, primary_instance) - .witness(witness, witness_addr, witness_instance) - .build_prod(); - - let handle = supervisor.handle(); - - std::thread::spawn(|| supervisor.run()); - - loop { - match handle.verify_to_highest() { - Ok(light_block) => { - println!("[info] synced to block {}", light_block.height()); - }, - Err(err) => { - println!("[error] sync failed: {err}"); - }, - } - - std::thread::sleep(Duration::from_millis(800)); - } -} diff --git a/light-client/src/builder/light_client.rs b/light-client/src/builder/light_client.rs index 625778d53..8aaf9d9f2 100644 --- a/light-client/src/builder/light_client.rs +++ b/light-client/src/builder/light_client.rs @@ -19,10 +19,10 @@ use crate::{ io::{AtHeight, Io}, scheduler::Scheduler, }, + instance::Instance, light_client::LightClient, state::{State, VerificationTrace}, store::LightStore, - supervisor::Instance, verifier::{ options::Options, predicates::VerificationPredicates, @@ -128,13 +128,11 @@ where } /// Set the given light block as the initial trusted state. - fn trust_light_block( + pub fn trust_light_block( mut self, trusted_state: LightBlock, ) -> Result, Error> { self.validate(&trusted_state)?; - - // TODO(liamsi, romac): it is unclear if this should be Trusted or only Verified self.light_store.insert(trusted_state, Status::Trusted); Ok(self.with_state(HasTrustedState)) diff --git a/light-client/src/components/clock.rs b/light-client/src/components/clock.rs index 18a6bfd0e..843385ab3 100644 --- a/light-client/src/components/clock.rs +++ b/light-client/src/components/clock.rs @@ -13,7 +13,7 @@ pub trait Clock: Send + Sync { } /// Provides the current wall clock time. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct SystemClock; impl Clock for SystemClock { fn now(&self) -> Time { @@ -22,3 +22,20 @@ impl Clock for SystemClock { .expect("system clock produces invalid time") } } + +#[derive(Copy, Clone, Debug)] +pub struct FixedClock { + now: Time, +} + +impl FixedClock { + pub fn new(now: Time) -> Self { + Self { now } + } +} + +impl Clock for FixedClock { + fn now(&self) -> Time { + self.now + } +} diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index 8aca7b4e4..4b2f559ee 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -45,6 +45,16 @@ define_error! { "invalid height: given height must be greater than 0" }, + HeightTooHigh + { + height: Height, + latest_height: Height, + } + |e| { + format_args!("height ({0}) is higher than latest height ({1})", + e.height, e.latest_height) + }, + InvalidValidatorSet [ tendermint::Error ] | _ | { "fetched validator set is invalid" }, @@ -64,6 +74,33 @@ define_error! { } } +impl IoError { + pub fn from_rpc(err: rpc::Error) -> Self { + Self::from_height_too_high(&err).unwrap_or_else(|| Self::rpc(err)) + } + + pub fn from_height_too_high(err: &rpc::Error) -> Option { + use regex::Regex; + + let err_str = err.to_string(); + + if err_str.contains("must be less than or equal to") { + let re = Regex::new( + r"height (\d+) must be less than or equal to the current blockchain height (\d+)", + ) + .ok()?; + + let captures = re.captures(&err_str)?; + let height = Height::try_from(captures[1].parse::().ok()?).ok()?; + let latest_height = Height::try_from(captures[2].parse::().ok()?).ok()?; + + Some(Self::height_too_high(height, latest_height)) + } else { + None + } + } +} + impl IoErrorDetail { /// Whether this error means that a timeout occurred when querying a node. pub fn is_timeout(&self) -> Option { @@ -151,7 +188,19 @@ mod prod { } } - fn fetch_signed_header(&self, height: AtHeight) -> Result { + pub fn peer_id(&self) -> PeerId { + self.peer_id + } + + pub fn rpc_client(&self) -> &rpc::HttpClient { + &self.rpc_client + } + + pub fn timeout(&self) -> Option { + self.timeout + } + + pub fn fetch_signed_header(&self, height: AtHeight) -> Result { let client = self.rpc_client.clone(); let res = block_on(self.timeout, async move { match height { @@ -162,11 +211,11 @@ mod prod { match res { Ok(response) => Ok(response.signed_header), - Err(err) => Err(IoError::rpc(err)), + Err(err) => Err(IoError::from_rpc(err)), } } - fn fetch_validator_set( + pub fn fetch_validator_set( &self, height: AtHeight, proposer_address: Option, diff --git a/light-client/src/errors.rs b/light-client/src/errors.rs index 45538799c..592a0c361 100644 --- a/light-client/src/errors.rs +++ b/light-client/src/errors.rs @@ -66,6 +66,16 @@ define_error! { e.target_height, e.trusted_height) }, + HeightTooHigh + { + height: Height, + latest_height: Height, + } + |e| { + format_args!("height ({0}) is higher than latest height ({1})", + e.height, e.latest_height) + }, + TrustedStateOutsideTrustingPeriod { trusted_state: Box, @@ -145,6 +155,14 @@ impl ErrorExt for ErrorDetail { None } } + + fn is_io(&self) -> bool { + matches!(self, Self::Io(_)) + } + + fn is_height_too_high(&self) -> bool { + matches!(self, Self::HeightTooHigh { .. }) + } } impl Error { diff --git a/light-client/src/instance.rs b/light-client/src/instance.rs new file mode 100644 index 000000000..7a3d141fc --- /dev/null +++ b/light-client/src/instance.rs @@ -0,0 +1,65 @@ +//! Supervisor and Handle implementation. + +use tendermint::block::Height; + +use crate::{ + errors::Error, + light_client::LightClient, + state::State, + verifier::types::{LightBlock, Status}, +}; + +/// A light client `Instance` packages a `LightClient` together with its `State`. +#[derive(Debug)] +pub struct Instance { + /// The light client for this instance + pub light_client: LightClient, + + /// The state of the light client for this instance + pub state: State, +} + +impl Instance { + /// Constructs a new instance from the given light client and its state. + pub fn new(light_client: LightClient, state: State) -> Self { + Self { + light_client, + state, + } + } + + /// Return the peer id of this instance. + pub fn peer_id(&self) -> &tendermint::node::Id { + &self.light_client.peer + } + + /// Get the latest trusted block. + pub fn latest_trusted(&self) -> Option { + self.state.light_store.highest(Status::Trusted) + } + + /// Trust the given block. + pub fn trust_block(&mut self, lb: &LightBlock) { + self.state.light_store.update(lb, Status::Trusted); + } + + /// Get or fetch the block at the given height + pub fn get_or_fetch_block(&mut self, height: Height) -> Result { + let (block, _) = self + .light_client + .get_or_fetch_block(height, &mut self.state) + .map_err(|e| { + // FIXME: Move this to the light client method + if e.to_string() + .contains("must be less than or equal to the current blockchain height") + { + // FIXME: Fetch latest height from error message + Error::height_too_high(height, Height::default()) + } else { + e + } + })?; + + Ok(block) + } +} diff --git a/light-client/src/lib.rs b/light-client/src/lib.rs index b10e86778..a26d94ed4 100644 --- a/light-client/src/lib.rs +++ b/light-client/src/lib.rs @@ -21,6 +21,7 @@ pub mod contracts; pub mod errors; pub mod evidence; pub mod fork_detector; +pub mod instance; pub mod light_client; pub mod peer_list; pub mod state; diff --git a/light-client/src/light_client.rs b/light-client/src/light_client.rs index e97f5d948..eabca1750 100644 --- a/light-client/src/light_client.rs +++ b/light-client/src/light_client.rs @@ -224,6 +224,9 @@ impl LightClient { // the `Verified` status or higher if already trusted. let new_status = Status::most_trusted(Status::Verified, status); state.light_store.update(¤t_block, new_status); + + // Log the trusted height as a dependency of the block at the current height + state.trace_block(current_height, trusted_block.height()); }, Verdict::Invalid(e) => { // Verification failed, add the block to the light store with `Failed` status, @@ -381,4 +384,40 @@ impl LightClient { Ok((block, Status::Unverified)) } + + /// Get the block at the given height or the latest block from the chain if the given height is + /// lower than the latest height. + pub fn get_target_block_or_latest( + &mut self, + height: Height, + state: &mut State, + ) -> Result { + let block = state.light_store.get_non_failed(height); + + if let Some((block, _)) = block { + return Ok(TargetOrLatest::Target(block)); + } + + let block = self.io.fetch_light_block(AtHeight::At(height)); + + if let Ok(block) = block { + return Ok(TargetOrLatest::Target(block)); + } + + let latest = self + .io + .fetch_light_block(AtHeight::Highest) + .map_err(Error::io)?; + + if latest.height() == height { + Ok(TargetOrLatest::Target(latest)) + } else { + Ok(TargetOrLatest::Latest(latest)) + } + } +} + +pub enum TargetOrLatest { + Latest(LightBlock), + Target(LightBlock), } diff --git a/light-client/src/state.rs b/light-client/src/state.rs index 392571e16..8f6652899 100644 --- a/light-client/src/state.rs +++ b/light-client/src/state.rs @@ -6,7 +6,7 @@ use contracts::*; use crate::{ store::LightStore, - verifier::types::{Height, LightBlock, Status}, + verifier::types::{Height, LightBlock}, }; /// Records which blocks were needed to verify a target block, eg. during bisection. @@ -39,7 +39,11 @@ impl State { pub fn trace_block(&mut self, target_height: Height, height: Height) { self.verification_trace .entry(target_height) - .or_insert_with(HashSet::new) + .or_insert_with(|| { + let mut trace = HashSet::new(); + trace.insert(target_height); + trace + }) .insert(height); } @@ -50,11 +54,10 @@ impl State { .get(&target_height) .unwrap_or(&HashSet::new()) .iter() - .flat_map(|h| self.light_store.get(*h, Status::Verified)) + .flat_map(|&height| self.light_store.get_trusted_or_verified(height)) .collect::>(); trace.sort_by_key(|lb| lb.height()); - trace.reverse(); trace } } diff --git a/light-client/src/supervisor.rs b/light-client/src/supervisor.rs index 6653b1551..030b04422 100644 --- a/light-client/src/supervisor.rs +++ b/light-client/src/supervisor.rs @@ -1,7 +1,6 @@ //! Supervisor and Handle implementation. use crossbeam_channel as channel; -use tendermint::evidence::Evidence; use crate::{ errors::Error, @@ -123,7 +122,7 @@ pub struct Supervisor { /// An instance of the fork detector fork_detector: Box, /// Reporter of fork evidence - evidence_reporter: Box, + _evidence_reporter: Box, /// Channel through which to reply to `Handle`s sender: channel::Sender, /// Channel through which to receive events from the `Handle`s @@ -155,7 +154,7 @@ impl Supervisor { sender, receiver, fork_detector: Box::new(fork_detector), - evidence_reporter: Box::new(evidence_reporter), + _evidence_reporter: Box::new(evidence_reporter), } } @@ -284,18 +283,14 @@ impl Supervisor { } /// Report the given evidence of a fork. - // TODO: rework to supply LightClientAttackEvidence data fn report_evidence( &mut self, - provider: PeerId, + _provider: PeerId, _primary: &LightBlock, _witness: &LightBlock, ) -> Result<(), Error> { - self.evidence_reporter - .report(Evidence::LightClientAttackEvidence, provider) - .map_err(Error::io)?; - - Ok(()) + // FIXME: Will be removed in a subsequent PR + todo!() } /// Perform fork detection with the given verified block and trusted block. @@ -730,6 +725,7 @@ mod tests { } #[test] + #[ignore] fn test_bisection_fork_detected() { let mut chain = LightChain::default_with_length(5); let primary = chain diff --git a/proto/src/prost/v0_34/tendermint.abci.rs b/proto/src/prost/v0_34/tendermint.abci.rs index ae59b8286..fbc793b73 100644 --- a/proto/src/prost/v0_34/tendermint.abci.rs +++ b/proto/src/prost/v0_34/tendermint.abci.rs @@ -397,17 +397,7 @@ pub struct ResponseOfferSnapshot { } /// Nested message and enum types in `ResponseOfferSnapshot`. pub mod response_offer_snapshot { - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum Result { /// Unknown result, abort all snapshot restoration @@ -472,17 +462,7 @@ pub struct ResponseApplySnapshotChunk { } /// Nested message and enum types in `ResponseApplySnapshotChunk`. pub mod response_apply_snapshot_chunk { - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum Result { /// Unknown result, abort all snapshot restoration diff --git a/proto/src/prost/v0_34/tendermint.crypto.rs b/proto/src/prost/v0_34/tendermint.crypto.rs index a6273013b..cfd024d19 100644 --- a/proto/src/prost/v0_34/tendermint.crypto.rs +++ b/proto/src/prost/v0_34/tendermint.crypto.rs @@ -56,7 +56,6 @@ pub struct ProofOps { pub ops: ::prost::alloc::vec::Vec, } /// PublicKey defines the keys available for use with Validators -#[derive(::serde::Deserialize, ::serde::Serialize)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PublicKey { diff --git a/proto/src/prost/v0_34/tendermint.p2p.rs b/proto/src/prost/v0_34/tendermint.p2p.rs index bfaa808cb..c95d86641 100644 --- a/proto/src/prost/v0_34/tendermint.p2p.rs +++ b/proto/src/prost/v0_34/tendermint.p2p.rs @@ -1,48 +1,5 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketPing {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketPong {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketMsg { - #[prost(int32, tag = "1")] - pub channel_id: i32, - #[prost(bool, tag = "2")] - pub eof: bool, - #[prost(bytes = "vec", tag = "3")] - pub data: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Packet { - #[prost(oneof = "packet::Sum", tags = "1, 2, 3")] - pub sum: ::core::option::Option, -} -/// Nested message and enum types in `Packet`. -pub mod packet { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Sum { - #[prost(message, tag = "1")] - PacketPing(super::PacketPing), - #[prost(message, tag = "2")] - PacketPong(super::PacketPong), - #[prost(message, tag = "3")] - PacketMsg(super::PacketMsg), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AuthSigMessage { - #[prost(message, optional, tag = "1")] - pub pub_key: ::core::option::Option, - #[prost(bytes = "vec", tag = "2")] - pub sig: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct NetAddress { #[prost(string, tag = "1")] pub id: ::prost::alloc::string::String, @@ -91,6 +48,49 @@ pub struct DefaultNodeInfoOther { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketPing {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketPong {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketMsg { + #[prost(int32, tag = "1")] + pub channel_id: i32, + #[prost(bool, tag = "2")] + pub eof: bool, + #[prost(bytes = "vec", tag = "3")] + pub data: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Packet { + #[prost(oneof = "packet::Sum", tags = "1, 2, 3")] + pub sum: ::core::option::Option, +} +/// Nested message and enum types in `Packet`. +pub mod packet { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Sum { + #[prost(message, tag = "1")] + PacketPing(super::PacketPing), + #[prost(message, tag = "2")] + PacketPong(super::PacketPong), + #[prost(message, tag = "3")] + PacketMsg(super::PacketMsg), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthSigMessage { + #[prost(message, optional, tag = "1")] + pub pub_key: ::core::option::Option, + #[prost(bytes = "vec", tag = "2")] + pub sig: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PexRequest {} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/prost/v0_34/tendermint.types.rs b/proto/src/prost/v0_34/tendermint.types.rs index 229dec5d9..c1001662a 100644 --- a/proto/src/prost/v0_34/tendermint.types.rs +++ b/proto/src/prost/v0_34/tendermint.types.rs @@ -7,6 +7,7 @@ pub struct ValidatorSet { #[prost(message, optional, tag = "2")] pub proposer: ::core::option::Option, #[prost(int64, tag = "3")] + #[serde(skip)] pub total_voting_power: i64, } #[derive(::serde::Deserialize, ::serde::Serialize)] @@ -64,7 +65,7 @@ pub struct BlockId { #[serde(with = "crate::serializers::bytes::hexstring")] pub hash: ::prost::alloc::vec::Vec, #[prost(message, optional, tag = "2")] - #[serde(alias = "parts")] + #[serde(rename = "parts", alias = "part_set_header")] pub part_set_header: ::core::option::Option, } /// Header defines the structure of a block header. @@ -268,8 +269,19 @@ pub struct TxProof { pub proof: ::core::option::Option, } /// BlockIdFlag indicates which BlcokID the signature is for -#[derive(::num_derive::FromPrimitive, ::num_derive::ToPrimitive)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[derive( + ::num_derive::FromPrimitive, + ::num_derive::ToPrimitive, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration, +)] #[repr(i32)] pub enum BlockIdFlag { Unknown = 0, @@ -336,6 +348,16 @@ impl SignedMsgType { } } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventDataRoundState { + #[prost(int64, tag = "1")] + pub height: i64, + #[prost(int32, tag = "2")] + pub round: i32, + #[prost(string, tag = "3")] + pub step: ::prost::alloc::string::String, +} /// ConsensusParams contains consensus critical parameters that determine the /// validity of blocks. #[allow(clippy::derive_partial_eq_without_eq)] @@ -379,6 +401,7 @@ pub struct EvidenceParams { /// The basic formula for calculating this is: MaxAgeDuration / {average block /// time}. #[prost(int64, tag = "1")] + #[serde(with = "crate::serializers::from_str", default)] pub max_age_num_blocks: i64, /// Max age of evidence, in time. /// @@ -422,16 +445,6 @@ pub struct HashedParams { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct EventDataRoundState { - #[prost(int64, tag = "1")] - pub height: i64, - #[prost(int32, tag = "2")] - pub round: i32, - #[prost(string, tag = "3")] - pub step: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct Evidence { #[prost(oneof = "evidence::Sum", tags = "1, 2")] pub sum: ::core::option::Option, @@ -461,27 +474,30 @@ pub struct DuplicateVoteEvidence { #[prost(message, optional, tag = "2")] pub vote_b: ::core::option::Option, #[prost(int64, tag = "3")] - #[serde(alias = "TotalVotingPower", with = "crate::serializers::from_str")] + #[serde(rename = "TotalVotingPower", with = "crate::serializers::from_str")] pub total_voting_power: i64, #[prost(int64, tag = "4")] - #[serde(alias = "ValidatorPower", with = "crate::serializers::from_str")] + #[serde(rename = "ValidatorPower", with = "crate::serializers::from_str")] pub validator_power: i64, #[prost(message, optional, tag = "5")] - #[serde(alias = "Timestamp")] + #[serde(rename = "Timestamp")] pub timestamp: ::core::option::Option, } /// LightClientAttackEvidence contains evidence of a set of validators attempting to mislead a light client. #[derive(::serde::Deserialize, ::serde::Serialize)] +#[serde(rename_all = "PascalCase")] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct LightClientAttackEvidence { #[prost(message, optional, tag = "1")] pub conflicting_block: ::core::option::Option, #[prost(int64, tag = "2")] + #[serde(with = "crate::serializers::from_str")] pub common_height: i64, #[prost(message, repeated, tag = "3")] pub byzantine_validators: ::prost::alloc::vec::Vec, #[prost(int64, tag = "4")] + #[serde(with = "crate::serializers::from_str")] pub total_voting_power: i64, #[prost(message, optional, tag = "5")] pub timestamp: ::core::option::Option, @@ -497,24 +513,10 @@ pub struct EvidenceList { #[derive(::serde::Deserialize, ::serde::Serialize)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Block { - #[prost(message, optional, tag = "1")] - pub header: ::core::option::Option
, - #[prost(message, optional, tag = "2")] - pub data: ::core::option::Option, - #[prost(message, optional, tag = "3")] - pub evidence: ::core::option::Option, - #[prost(message, optional, tag = "4")] - pub last_commit: ::core::option::Option, -} -#[derive(::serde::Deserialize, ::serde::Serialize)] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct CanonicalBlockId { #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, #[prost(message, optional, tag = "2")] - #[serde(alias = "parts")] pub part_set_header: ::core::option::Option, } #[derive(::serde::Deserialize, ::serde::Serialize)] @@ -567,3 +569,16 @@ pub struct CanonicalVote { #[prost(string, tag = "6")] pub chain_id: ::prost::alloc::string::String, } +#[derive(::serde::Deserialize, ::serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Block { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option
, + #[prost(message, optional, tag = "2")] + pub data: ::core::option::Option, + #[prost(message, optional, tag = "3")] + pub evidence: ::core::option::Option, + #[prost(message, optional, tag = "4")] + pub last_commit: ::core::option::Option, +} diff --git a/proto/src/prost/v0_37/tendermint.crypto.rs b/proto/src/prost/v0_37/tendermint.crypto.rs index a6273013b..cfd024d19 100644 --- a/proto/src/prost/v0_37/tendermint.crypto.rs +++ b/proto/src/prost/v0_37/tendermint.crypto.rs @@ -56,7 +56,6 @@ pub struct ProofOps { pub ops: ::prost::alloc::vec::Vec, } /// PublicKey defines the keys available for use with Validators -#[derive(::serde::Deserialize, ::serde::Serialize)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PublicKey { diff --git a/proto/src/prost/v0_37/tendermint.p2p.rs b/proto/src/prost/v0_37/tendermint.p2p.rs index bfaa808cb..c95d86641 100644 --- a/proto/src/prost/v0_37/tendermint.p2p.rs +++ b/proto/src/prost/v0_37/tendermint.p2p.rs @@ -1,48 +1,5 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketPing {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketPong {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketMsg { - #[prost(int32, tag = "1")] - pub channel_id: i32, - #[prost(bool, tag = "2")] - pub eof: bool, - #[prost(bytes = "vec", tag = "3")] - pub data: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Packet { - #[prost(oneof = "packet::Sum", tags = "1, 2, 3")] - pub sum: ::core::option::Option, -} -/// Nested message and enum types in `Packet`. -pub mod packet { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Sum { - #[prost(message, tag = "1")] - PacketPing(super::PacketPing), - #[prost(message, tag = "2")] - PacketPong(super::PacketPong), - #[prost(message, tag = "3")] - PacketMsg(super::PacketMsg), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AuthSigMessage { - #[prost(message, optional, tag = "1")] - pub pub_key: ::core::option::Option, - #[prost(bytes = "vec", tag = "2")] - pub sig: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct NetAddress { #[prost(string, tag = "1")] pub id: ::prost::alloc::string::String, @@ -91,6 +48,49 @@ pub struct DefaultNodeInfoOther { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketPing {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketPong {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketMsg { + #[prost(int32, tag = "1")] + pub channel_id: i32, + #[prost(bool, tag = "2")] + pub eof: bool, + #[prost(bytes = "vec", tag = "3")] + pub data: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Packet { + #[prost(oneof = "packet::Sum", tags = "1, 2, 3")] + pub sum: ::core::option::Option, +} +/// Nested message and enum types in `Packet`. +pub mod packet { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Sum { + #[prost(message, tag = "1")] + PacketPing(super::PacketPing), + #[prost(message, tag = "2")] + PacketPong(super::PacketPong), + #[prost(message, tag = "3")] + PacketMsg(super::PacketMsg), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthSigMessage { + #[prost(message, optional, tag = "1")] + pub pub_key: ::core::option::Option, + #[prost(bytes = "vec", tag = "2")] + pub sig: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PexRequest {} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/prost/v0_37/tendermint.types.rs b/proto/src/prost/v0_37/tendermint.types.rs index 3ccc14ee6..ff28c0eb2 100644 --- a/proto/src/prost/v0_37/tendermint.types.rs +++ b/proto/src/prost/v0_37/tendermint.types.rs @@ -7,6 +7,7 @@ pub struct ValidatorSet { #[prost(message, optional, tag = "2")] pub proposer: ::core::option::Option, #[prost(int64, tag = "3")] + #[serde(skip)] pub total_voting_power: i64, } #[derive(::serde::Deserialize, ::serde::Serialize)] @@ -64,7 +65,7 @@ pub struct BlockId { #[serde(with = "crate::serializers::bytes::hexstring")] pub hash: ::prost::alloc::vec::Vec, #[prost(message, optional, tag = "2")] - #[serde(alias = "parts")] + #[serde(rename = "parts", alias = "part_set_header")] pub part_set_header: ::core::option::Option, } /// Header defines the structure of a block header. @@ -268,8 +269,19 @@ pub struct TxProof { pub proof: ::core::option::Option, } /// BlockIdFlag indicates which BlcokID the signature is for -#[derive(::num_derive::FromPrimitive, ::num_derive::ToPrimitive)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[derive( + ::num_derive::FromPrimitive, + ::num_derive::ToPrimitive, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration, +)] #[repr(i32)] pub enum BlockIdFlag { Unknown = 0, @@ -336,6 +348,96 @@ impl SignedMsgType { } } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Evidence { + #[prost(oneof = "evidence::Sum", tags = "1, 2")] + pub sum: ::core::option::Option, +} +/// Nested message and enum types in `Evidence`. +pub mod evidence { + #[derive(::serde::Deserialize, ::serde::Serialize)] + #[serde(tag = "type", content = "value")] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Sum { + #[prost(message, tag = "1")] + #[serde(rename = "tendermint/DuplicateVoteEvidence")] + DuplicateVoteEvidence(super::DuplicateVoteEvidence), + #[prost(message, tag = "2")] + #[serde(rename = "tendermint/LightClientAttackEvidence")] + LightClientAttackEvidence(super::LightClientAttackEvidence), + } +} +/// DuplicateVoteEvidence contains evidence of a validator signed two conflicting votes. +#[derive(::serde::Deserialize, ::serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DuplicateVoteEvidence { + #[prost(message, optional, tag = "1")] + pub vote_a: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub vote_b: ::core::option::Option, + #[prost(int64, tag = "3")] + #[serde(rename = "TotalVotingPower", with = "crate::serializers::from_str")] + pub total_voting_power: i64, + #[prost(int64, tag = "4")] + #[serde(rename = "ValidatorPower", with = "crate::serializers::from_str")] + pub validator_power: i64, + #[prost(message, optional, tag = "5")] + #[serde(rename = "Timestamp")] + pub timestamp: ::core::option::Option, +} +/// LightClientAttackEvidence contains evidence of a set of validators attempting to mislead a light client. +#[derive(::serde::Deserialize, ::serde::Serialize)] +#[serde(rename_all = "PascalCase")] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LightClientAttackEvidence { + #[prost(message, optional, tag = "1")] + pub conflicting_block: ::core::option::Option, + #[prost(int64, tag = "2")] + #[serde(with = "crate::serializers::from_str")] + pub common_height: i64, + #[prost(message, repeated, tag = "3")] + pub byzantine_validators: ::prost::alloc::vec::Vec, + #[prost(int64, tag = "4")] + #[serde(with = "crate::serializers::from_str")] + pub total_voting_power: i64, + #[prost(message, optional, tag = "5")] + pub timestamp: ::core::option::Option, +} +#[derive(::serde::Deserialize, ::serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EvidenceList { + #[prost(message, repeated, tag = "1")] + #[serde(with = "crate::serializers::nullable")] + pub evidence: ::prost::alloc::vec::Vec, +} +#[derive(::serde::Deserialize, ::serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Block { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option
, + #[prost(message, optional, tag = "2")] + pub data: ::core::option::Option, + #[prost(message, optional, tag = "3")] + pub evidence: ::core::option::Option, + #[prost(message, optional, tag = "4")] + pub last_commit: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventDataRoundState { + #[prost(int64, tag = "1")] + pub height: i64, + #[prost(int32, tag = "2")] + pub round: i32, + #[prost(string, tag = "3")] + pub step: ::prost::alloc::string::String, +} /// ConsensusParams contains consensus critical parameters that determine the /// validity of blocks. #[allow(clippy::derive_partial_eq_without_eq)] @@ -373,6 +475,7 @@ pub struct EvidenceParams { /// The basic formula for calculating this is: MaxAgeDuration / {average block /// time}. #[prost(int64, tag = "1")] + #[serde(with = "crate::serializers::from_str", default)] pub max_age_num_blocks: i64, /// Max age of evidence, in time. /// @@ -414,93 +517,6 @@ pub struct HashedParams { #[prost(int64, tag = "2")] pub block_max_gas: i64, } -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EventDataRoundState { - #[prost(int64, tag = "1")] - pub height: i64, - #[prost(int32, tag = "2")] - pub round: i32, - #[prost(string, tag = "3")] - pub step: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Evidence { - #[prost(oneof = "evidence::Sum", tags = "1, 2")] - pub sum: ::core::option::Option, -} -/// Nested message and enum types in `Evidence`. -pub mod evidence { - #[derive(::serde::Deserialize, ::serde::Serialize)] - #[serde(tag = "type", content = "value")] - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Sum { - #[prost(message, tag = "1")] - #[serde(rename = "tendermint/DuplicateVoteEvidence")] - DuplicateVoteEvidence(super::DuplicateVoteEvidence), - #[prost(message, tag = "2")] - #[serde(rename = "tendermint/LightClientAttackEvidence")] - LightClientAttackEvidence(super::LightClientAttackEvidence), - } -} -/// DuplicateVoteEvidence contains evidence of a validator signed two conflicting votes. -#[derive(::serde::Deserialize, ::serde::Serialize)] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DuplicateVoteEvidence { - #[prost(message, optional, tag = "1")] - pub vote_a: ::core::option::Option, - #[prost(message, optional, tag = "2")] - pub vote_b: ::core::option::Option, - #[prost(int64, tag = "3")] - #[serde(alias = "TotalVotingPower", with = "crate::serializers::from_str")] - pub total_voting_power: i64, - #[prost(int64, tag = "4")] - #[serde(alias = "ValidatorPower", with = "crate::serializers::from_str")] - pub validator_power: i64, - #[prost(message, optional, tag = "5")] - #[serde(alias = "Timestamp")] - pub timestamp: ::core::option::Option, -} -/// LightClientAttackEvidence contains evidence of a set of validators attempting to mislead a light client. -#[derive(::serde::Deserialize, ::serde::Serialize)] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct LightClientAttackEvidence { - #[prost(message, optional, tag = "1")] - pub conflicting_block: ::core::option::Option, - #[prost(int64, tag = "2")] - pub common_height: i64, - #[prost(message, repeated, tag = "3")] - pub byzantine_validators: ::prost::alloc::vec::Vec, - #[prost(int64, tag = "4")] - pub total_voting_power: i64, - #[prost(message, optional, tag = "5")] - pub timestamp: ::core::option::Option, -} -#[derive(::serde::Deserialize, ::serde::Serialize)] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EvidenceList { - #[prost(message, repeated, tag = "1")] - #[serde(with = "crate::serializers::nullable")] - pub evidence: ::prost::alloc::vec::Vec, -} -#[derive(::serde::Deserialize, ::serde::Serialize)] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Block { - #[prost(message, optional, tag = "1")] - pub header: ::core::option::Option
, - #[prost(message, optional, tag = "2")] - pub data: ::core::option::Option, - #[prost(message, optional, tag = "3")] - pub evidence: ::core::option::Option, - #[prost(message, optional, tag = "4")] - pub last_commit: ::core::option::Option, -} #[derive(::serde::Deserialize, ::serde::Serialize)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -508,7 +524,6 @@ pub struct CanonicalBlockId { #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, #[prost(message, optional, tag = "2")] - #[serde(alias = "parts")] pub part_set_header: ::core::option::Option, } #[derive(::serde::Deserialize, ::serde::Serialize)] diff --git a/proto/src/serializers.rs b/proto/src/serializers.rs index 161833816..5780154c0 100644 --- a/proto/src/serializers.rs +++ b/proto/src/serializers.rs @@ -55,12 +55,13 @@ pub mod allow_null; pub mod bytes; -mod evidence; +pub mod evidence; pub mod from_str; pub mod nullable; pub mod optional; pub mod optional_from_str; pub mod part_set_header_total; +pub mod public_key; pub mod time_duration; pub mod timestamp; pub mod txs; diff --git a/proto/src/serializers/public_key.rs b/proto/src/serializers/public_key.rs new file mode 100644 index 000000000..49a6fda02 --- /dev/null +++ b/proto/src/serializers/public_key.rs @@ -0,0 +1,47 @@ +mod v0_34 { + use crate::v0_34::crypto::{public_key, PublicKey}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let sum = Option::::deserialize(deserializer)?; + Ok(Self { sum }) + } + } + + impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.sum.serialize(serializer) + } + } +} + +mod v0_37 { + use crate::v0_37::crypto::{public_key, PublicKey}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let sum = Option::::deserialize(deserializer)?; + Ok(Self { sum }) + } + } + + impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.sum.serialize(serializer) + } + } +} diff --git a/release.sh b/release.sh index b4419d07f..6df2cde69 100755 --- a/release.sh +++ b/release.sh @@ -18,7 +18,7 @@ set -e # A space-separated list of all the crates we want to publish, in the order in # which they must be published. It's important to respect this order, since # each subsequent crate depends on one or more of the preceding ones. -DEFAULT_CRATES="tendermint-proto tendermint-std-ext tendermint tendermint-config tendermint-abci tendermint-rpc tendermint-p2p tendermint-light-client-verifier tendermint-light-client tendermint-light-client-js tendermint-testgen" +DEFAULT_CRATES="tendermint-proto tendermint-std-ext tendermint tendermint-config tendermint-abci tendermint-rpc tendermint-p2p tendermint-light-client-verifier tendermint-light-client tendermint-light-client-detector tendermint-light-client-js tendermint-testgen" # Allows us to override the crates we want to publish. CRATES=${*:-${DEFAULT_CRATES}} diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 1a07d71f1..9584e124e 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -60,6 +60,10 @@ websocket-client = [ ] [dependencies] +tendermint = { version = "0.31.1", default-features = false, path = "../tendermint" } +tendermint-config = { version = "0.31.1", path = "../config", default-features = false } +tendermint-proto = { version = "0.31.1", path = "../proto", default-features = false } + async-trait = { version = "0.1", default-features = false } bytes = { version = "1.0", default-features = false } getrandom = { version = "0.2", default-features = false, features = ["js"] } @@ -68,8 +72,6 @@ pin-project = { version = "1.0.1", default-features = false } serde = { version = "1", default-features = false, features = [ "derive" ] } serde_bytes = { version = "0.11", default-features = false } serde_json = { version = "1", default-features = false, features = ["std"] } -tendermint-config = { version = "0.31.1", path = "../config", default-features = false } -tendermint = { version = "0.31.1", default-features = false, path = "../tendermint" } thiserror = { version = "1", default-features = false } time = { version = "0.3", default-features = false, features = ["macros", "parsing"] } uuid = { version = "0.8", default-features = false } diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index 648aa859f..2c89d4163 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -7,14 +7,13 @@ use core::{ use async_trait::async_trait; -use tendermint::{block::Height, Hash}; +use tendermint::{block::Height, evidence::Evidence, Hash}; use tendermint_config::net; -use crate::dialect::v0_34; use crate::prelude::*; use crate::{ client::{Client, CompatMode}, - endpoint, + dialect, endpoint, query::Query, Error, Order, Scheme, SimpleRequest, Url, }; @@ -160,7 +159,7 @@ impl HttpClient { async fn perform_v0_34(&self, request: R) -> Result where - R: SimpleRequest, + R: SimpleRequest, { self.inner.perform(request).await } @@ -220,6 +219,17 @@ impl Client for HttpClient { } } + /// `/broadcast_evidence`: broadcast an evidence. + async fn broadcast_evidence(&self, e: Evidence) -> Result { + match self.compat { + CompatMode::V0_37 => self.perform(endpoint::evidence::Request::new(e)).await, + CompatMode::V0_34 => { + self.perform_v0_34(endpoint::evidence::Request::new(e)) + .await + }, + } + } + async fn tx(&self, hash: Hash, prove: bool) -> Result { perform_with_compat!(self, endpoint::tx::Request::new(hash, prove)) } diff --git a/rpc/src/dialect.rs b/rpc/src/dialect.rs index ddcb94748..176fe59b9 100644 --- a/rpc/src/dialect.rs +++ b/rpc/src/dialect.rs @@ -16,10 +16,11 @@ pub use end_block::EndBlock; use serde::{de::DeserializeOwned, Serialize}; -use tendermint::abci; +use tendermint::{abci, evidence}; pub trait Dialect: sealed::Sealed + Default + Clone + Send + Sync { type Event: Into + Serialize + DeserializeOwned; + type Evidence: From + Serialize + DeserializeOwned + Send; } pub type LatestDialect = v0_37::Dialect; diff --git a/rpc/src/dialect/v0_34.rs b/rpc/src/dialect/v0_34.rs index 6e972b1d7..2102b71fa 100644 --- a/rpc/src/dialect/v0_34.rs +++ b/rpc/src/dialect/v0_34.rs @@ -1,7 +1,9 @@ -use tendermint::abci; +use tendermint::{abci, evidence}; +use tendermint_proto::v0_34::types::Evidence as RawEvidence; use crate::prelude::*; use crate::serializers::bytes::base64string; + use serde::{Deserialize, Serialize}; #[derive(Default, Clone)] @@ -9,6 +11,7 @@ pub struct Dialect; impl crate::dialect::Dialect for Dialect { type Event = Event; + type Evidence = Evidence; } #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -75,3 +78,27 @@ impl From for EventAttribute { } } } + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(into = "RawEvidence", try_from = "RawEvidence")] +pub struct Evidence(evidence::Evidence); + +impl From for RawEvidence { + fn from(evidence: Evidence) -> Self { + evidence.0.into() + } +} + +impl TryFrom for Evidence { + type Error = >::Error; + + fn try_from(value: RawEvidence) -> Result { + Ok(Self(evidence::Evidence::try_from(value)?)) + } +} + +impl From for Evidence { + fn from(evidence: evidence::Evidence) -> Self { + Self(evidence) + } +} diff --git a/rpc/src/dialect/v0_37.rs b/rpc/src/dialect/v0_37.rs index 28cc49947..2f28501f7 100644 --- a/rpc/src/dialect/v0_37.rs +++ b/rpc/src/dialect/v0_37.rs @@ -1,4 +1,5 @@ -use tendermint::abci; +use tendermint::{abci, evidence}; +use tendermint_proto::v0_37 as raw; use crate::prelude::*; use serde::{Deserialize, Serialize}; @@ -8,6 +9,7 @@ pub struct Dialect; impl crate::dialect::Dialect for Dialect { type Event = Event; + type Evidence = Evidence; } #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -66,3 +68,27 @@ impl From for EventAttribute { } } } + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(into = "raw::types::Evidence", try_from = "raw::types::Evidence")] +pub struct Evidence(evidence::Evidence); + +impl From for raw::types::Evidence { + fn from(evidence: Evidence) -> Self { + evidence.0.into() + } +} + +impl TryFrom for Evidence { + type Error = >::Error; + + fn try_from(value: raw::types::Evidence) -> Result { + Ok(Self(evidence::Evidence::try_from(value)?)) + } +} + +impl From for Evidence { + fn from(evidence: evidence::Evidence) -> Self { + Self(evidence) + } +} diff --git a/rpc/src/endpoint/evidence.rs b/rpc/src/endpoint/evidence.rs index d45977174..2be300421 100644 --- a/rpc/src/endpoint/evidence.rs +++ b/rpc/src/endpoint/evidence.rs @@ -1,35 +1,37 @@ //! `/broadcast_evidence`: broadcast an evidence. use serde::{Deserialize, Serialize}; -use tendermint::{evidence::Evidence, Hash}; +use tendermint::{evidence, Hash}; use crate::{dialect::Dialect, request::RequestMessage, Method}; /// `/broadcast_evidence`: broadcast an evidence. -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct Request { +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Request { /// Evidence to broadcast - pub ev: Evidence, + pub evidence: S::Evidence, } -impl Request { +impl Request { /// Create a new evidence broadcast RPC request - pub fn new(ev: Evidence) -> Request { - Request { ev } + pub fn new(evidence: evidence::Evidence) -> Self { + Request { + evidence: evidence.into(), + } } } -impl RequestMessage for Request { +impl RequestMessage for Request { fn method(&self) -> Method { Method::BroadcastEvidence } } -impl crate::Request for Request { +impl crate::Request for Request { type Response = Response; } -impl crate::SimpleRequest for Request { +impl crate::SimpleRequest for Request { type Output = Response; } @@ -37,6 +39,7 @@ impl crate::SimpleRequest for Request { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { /// Evidence hash + #[serde(with = "crate::serializers::tm_hash_base64")] pub hash: Hash, } diff --git a/rpc/tests/gaia_fixtures.rs b/rpc/tests/gaia_fixtures.rs index c6aa90c13..486913f6a 100644 --- a/rpc/tests/gaia_fixtures.rs +++ b/rpc/tests/gaia_fixtures.rs @@ -44,10 +44,10 @@ fn incoming_fixtures() { assert!(endpoint::block::Response::from_string(content).is_err()) }, "block_at_height_1" => { - assert!(endpoint::block::Response::from_string(content).is_ok()) + endpoint::block::Response::from_string(content).unwrap(); }, "block_at_height_10" => { - assert!(endpoint::block::Response::from_string(content).is_ok()) + endpoint::block::Response::from_string(content).unwrap(); }, "block_at_height_4555980" => { let r = endpoint::block::Response::from_string(content); @@ -62,27 +62,25 @@ fn incoming_fixtures() { assert!(r.is_ok(), "block_results_at_height_4555980: {r:?}"); }, "blockchain_from_1_to_10" => { - assert!(endpoint::blockchain::Response::from_string(content).is_ok()) + endpoint::blockchain::Response::from_string(content).unwrap(); }, "commit_at_height_10" => { - assert!(endpoint::commit::Response::from_string(content).is_ok()) + endpoint::commit::Response::from_string(content).unwrap(); }, "consensus_params" => { - assert!(endpoint::consensus_params::Response::from_string(content).is_ok()) + endpoint::consensus_params::Response::from_string(content).unwrap(); }, "consensus_state" => { - assert!(endpoint::consensus_state::Response::from_string(content).is_ok()) + endpoint::consensus_state::Response::from_string(content).unwrap(); }, "genesis" => { - assert!( - endpoint::genesis::Response::::from_string(content).is_ok() - ) + endpoint::genesis::Response::::from_string(content).unwrap(); }, "net_info" => { - assert!(endpoint::net_info::Response::from_string(content).is_ok()) + endpoint::net_info::Response::from_string(content).unwrap(); }, "status" => { - assert!(endpoint::status::Response::from_string(content).is_ok()) + endpoint::status::Response::from_string(content).unwrap(); }, "subscribe_newblock" => { let r = endpoint::subscribe::Response::from_string(content); @@ -117,16 +115,16 @@ fn outgoing_fixtures() { assert!(r.is_ok(), "{r:?}") }, "block_at_height_0" => { - assert!(endpoint::block::Request::from_string(content).is_ok()) + endpoint::block::Request::from_string(content).unwrap(); }, "block_at_height_1" => { - assert!(endpoint::block::Request::from_string(content).is_ok()) + endpoint::block::Request::from_string(content).unwrap(); }, "block_at_height_10" => { - assert!(endpoint::block::Request::from_string(content).is_ok()) + endpoint::block::Request::from_string(content).unwrap(); }, "block_at_height_4555980" => { - assert!(endpoint::block::Request::from_string(content).is_ok()) + endpoint::block::Request::from_string(content).unwrap(); }, "block_results_at_height_10" => { let r = endpoint::block_results::Request::from_string(content); @@ -137,28 +135,26 @@ fn outgoing_fixtures() { assert!(r.is_ok(), "block_results_at_height_4555980: {r:?}"); }, "blockchain_from_1_to_10" => { - assert!(endpoint::blockchain::Request::from_string(content).is_ok()) + endpoint::blockchain::Request::from_string(content).unwrap(); }, "commit_at_height_10" => { - assert!(endpoint::commit::Request::from_string(content).is_ok()) + endpoint::commit::Request::from_string(content).unwrap(); }, "consensus_params" => { - assert!(endpoint::consensus_params::Request::from_string(content).is_ok()) + endpoint::consensus_params::Request::from_string(content).unwrap(); }, "consensus_state" => { - assert!(endpoint::consensus_state::Request::from_string(content).is_ok()) + endpoint::consensus_state::Request::from_string(content).unwrap(); }, "genesis" => { - assert!( - endpoint::genesis::Request::>::from_string(content) - .is_ok() - ) + endpoint::genesis::Request::>::from_string(content) + .unwrap(); }, "net_info" => { - assert!(endpoint::net_info::Request::from_string(content).is_ok()) + endpoint::net_info::Request::from_string(content).unwrap(); }, "status" => { - assert!(endpoint::status::Request::from_string(content).is_ok()) + endpoint::status::Request::from_string(content).unwrap(); }, "subscribe_newblock" => { let r = endpoint::subscribe::Request::from_string(content); diff --git a/tendermint/src/block.rs b/tendermint/src/block.rs index f0471c598..2898d7a79 100644 --- a/tendermint/src/block.rs +++ b/tendermint/src/block.rs @@ -42,7 +42,7 @@ pub struct Block { pub data: Vec>, /// Evidence of malfeasance - pub evidence: evidence::Data, + pub evidence: evidence::List, /// Last commit pub last_commit: Option, @@ -71,18 +71,17 @@ tendermint_pb_modules! { "last_commit is empty on non-first block".to_string(), )); } + // Todo: Figure out requirements. // if last_commit.is_some() && header.height.value() == 1 { // return Err(Kind::InvalidFirstBlock.context("last_commit is not null on first // height").into()); //} + Ok(Block { header, data: value.data.ok_or_else(Error::missing_data)?.txs, - evidence: value - .evidence - .ok_or_else(Error::missing_evidence)? - .try_into()?, + evidence: value.evidence.map(TryInto::try_into).transpose()?.unwrap_or_default(), last_commit, }) } @@ -106,7 +105,7 @@ impl Block { pub fn new( header: Header, data: Vec>, - evidence: evidence::Data, + evidence: evidence::List, last_commit: Option, ) -> Result { if last_commit.is_none() && header.height.value() != 1 { @@ -138,7 +137,7 @@ impl Block { } /// Get evidence - pub fn evidence(&self) -> &evidence::Data { + pub fn evidence(&self) -> &evidence::List { &self.evidence } diff --git a/tendermint/src/consensus/params.rs b/tendermint/src/consensus/params.rs index 10361050a..ac6295c60 100644 --- a/tendermint/src/consensus/params.rs +++ b/tendermint/src/consensus/params.rs @@ -7,7 +7,7 @@ use crate::{block, evidence, prelude::*, public_key}; /// All consensus-relevant parameters that can be adjusted by the ABCI app. /// /// [ABCI documentation](https://docs.tendermint.com/master/spec/abci/abci.html#consensusparams) -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Params { /// Parameters limiting the size of a block and time between consecutive blocks. pub block: block::Size, @@ -16,8 +16,7 @@ pub struct Params { /// Parameters limiting the types of public keys validators can use. pub validator: ValidatorParams, /// The ABCI application version. - /// Version parameters - #[serde(skip)] // Todo: FIXME kvstore /genesis returns '{}' instead of '{app_version: "0"}' + #[serde(skip)] // FIXME: kvstore /genesis returns '{}' instead of '{app_version: "0"}' pub version: Option, } diff --git a/tendermint/src/error.rs b/tendermint/src/error.rs index dbcb93862..56abab5d9 100644 --- a/tendermint/src/error.rs +++ b/tendermint/src/error.rs @@ -6,7 +6,7 @@ use core::num::TryFromIntError; use flex_error::{define_error, DisplayOnly}; use serde::{Deserialize, Serialize}; -use crate::{account, vote}; +use crate::account; define_error! { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -191,10 +191,6 @@ define_error! { UnsupportedProcessProposalStatus |_| { format_args!("unsupported ProcessProposal status value" ) }, - RawVotingPowerMismatch - { raw: vote::Power, computed: vote::Power } - |e| { format_args!("mismatch between raw voting ({0:?}) and computed one ({1:?})", e.raw, e.computed) }, - NegativeMaxAgeNum [ DisplayOnly ] |_| { format_args!("negative max_age_num_blocks") }, diff --git a/tendermint/src/evidence.rs b/tendermint/src/evidence.rs index af735007a..8476fc7aa 100644 --- a/tendermint/src/evidence.rs +++ b/tendermint/src/evidence.rs @@ -7,25 +7,37 @@ use core::{ use serde::{Deserialize, Serialize}; use tendermint_proto::google::protobuf::Duration as RawDuration; -use tendermint_proto::v0_37::types::Evidence as RawEvidence; use tendermint_proto::Protobuf; -use crate::{error::Error, prelude::*, serializers, vote::Power, Time, Vote}; +use crate::{ + block::{signed_header::SignedHeader, Height}, + error::Error, + prelude::*, + serializers, validator, + vote::Power, + Time, Vote, +}; -/// Evidence of malfeasance by validators (i.e. signing conflicting votes). -/// encoded using an Amino prefix. There is currently only a single type of -/// evidence: `DuplicateVoteEvidence`. -/// -/// -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(try_from = "RawEvidence", into = "RawEvidence")] // Used by RPC /broadcast_evidence endpoint -#[allow(clippy::large_enum_variant)] +/// Evidence of malfeasance by validators (i.e. signing conflicting votes or light client attack). +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Evidence { /// Duplicate vote evidence - DuplicateVote(DuplicateVoteEvidence), + DuplicateVote(Box), + + /// LightClient attack evidence + LightClientAttack(Box), +} + +impl From for Evidence { + fn from(ev: LightClientAttackEvidence) -> Self { + Self::LightClientAttack(Box::new(ev)) + } +} - /// LightClient attack evidence - Todo: Implement details - LightClientAttackEvidence, +impl From for Evidence { + fn from(ev: DuplicateVoteEvidence) -> Self { + Self::DuplicateVote(Box::new(ev)) + } } /// Duplicate vote evidence @@ -44,6 +56,7 @@ impl DuplicateVoteEvidence { if vote_a.height != vote_b.height { return Err(Error::invalid_evidence()); } + // Todo: make more assumptions about what is considered a valid evidence for duplicate vote Ok(Self { vote_a, @@ -53,25 +66,43 @@ impl DuplicateVoteEvidence { timestamp: Time::unix_epoch(), }) } + /// Get votes pub fn votes(&self) -> (&Vote, &Vote) { (&self.vote_a, &self.vote_b) } } -/// Evidence data is a wrapper for a list of `Evidence`. +/// Conflicting block detected in light client attack +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConflictingBlock { + pub signed_header: SignedHeader, + pub validator_set: validator::Set, +} + +/// Light client attack evidence +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct LightClientAttackEvidence { + pub conflicting_block: ConflictingBlock, + pub common_height: Height, + pub byzantine_validators: Vec, + pub total_voting_power: Power, + pub timestamp: Time, +} + +/// A list of `Evidence`. /// /// -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Data(Vec); +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct List(Vec); -impl Data { +impl List { /// Create a new evidence data collection - pub fn new(into_evidence: I) -> Data + pub fn new(into_evidence: I) -> List where I: Into>, { - Data(into_evidence.into()) + List(into_evidence.into()) } /// Convert this evidence data into a vector @@ -85,7 +116,7 @@ impl Data { } } -impl AsRef<[Evidence]> for Data { +impl AsRef<[Evidence]> for List { fn as_ref(&self) -> &[Evidence] { &self.0 } @@ -94,9 +125,7 @@ impl AsRef<[Evidence]> for Data { /// EvidenceParams determine how we handle evidence of malfeasance. /// /// [Tendermint documentation](https://docs.tendermint.com/master/spec/core/data_structures.html#evidenceparams) -#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] -// Todo: This struct is ready to be converted through tendermint_proto::types::EvidenceParams. -// https://github.com/informalsystems/tendermint-rs/issues/741 +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Params { /// Max age of evidence, in blocks. #[serde(with = "serializers::from_str")] @@ -122,41 +151,47 @@ pub struct Params { // ============================================================================= tendermint_pb_modules! { - use pb::types::{ - evidence::Sum as RawSum, DuplicateVoteEvidence as RawDuplicateVoteEvidence, - Evidence as RawEvidence, EvidenceList as RawEvidenceList, - EvidenceParams as RawEvidenceParams, - }; + use pb::types as raw; - use super::{Data, DuplicateVoteEvidence, Evidence, Params}; + use super::{List, LightClientAttackEvidence, DuplicateVoteEvidence, ConflictingBlock, Evidence, Params}; use crate::{error::Error, prelude::*}; - impl TryFrom for Evidence { + impl Protobuf for Evidence {} + + impl TryFrom for Evidence { type Error = Error; - fn try_from(message: RawEvidence) -> Result { - use RawSum::*; - match message.sum.ok_or_else(Error::invalid_evidence)? { - DuplicateVoteEvidence(ev) => Ok(Evidence::DuplicateVote(ev.try_into()?)), - LightClientAttackEvidence(_ev) => Ok(Evidence::LightClientAttackEvidence), + fn try_from(value: raw::Evidence) -> Result { + match value.sum.ok_or_else(Error::invalid_evidence)? { + raw::evidence::Sum::DuplicateVoteEvidence(ev) => { + Ok(Evidence::DuplicateVote(Box::new(ev.try_into()?))) + }, + raw::evidence::Sum::LightClientAttackEvidence(ev) => { + Ok(Evidence::LightClientAttack(Box::new(ev.try_into()?))) + }, } } } - impl From for RawEvidence { + impl From for raw::Evidence { fn from(value: Evidence) -> Self { - let sum = match value { - Evidence::DuplicateVote(ev) => Some(RawSum::DuplicateVoteEvidence(ev.into())), - Evidence::LightClientAttackEvidence => None, - }; - RawEvidence { sum } + match value { + Evidence::DuplicateVote(ev) => raw::Evidence { + sum: Some(raw::evidence::Sum::DuplicateVoteEvidence((*ev).into())), + }, + Evidence::LightClientAttack(ev) => raw::Evidence { + sum: Some(raw::evidence::Sum::LightClientAttackEvidence((*ev).into())), + }, + } } } - impl TryFrom for DuplicateVoteEvidence { + impl Protobuf for DuplicateVoteEvidence {} + + impl TryFrom for DuplicateVoteEvidence { type Error = Error; - fn try_from(value: RawDuplicateVoteEvidence) -> Result { + fn try_from(value: raw::DuplicateVoteEvidence) -> Result { Ok(Self { vote_a: value .vote_a @@ -176,9 +211,9 @@ tendermint_pb_modules! { } } - impl From for RawDuplicateVoteEvidence { + impl From for raw::DuplicateVoteEvidence { fn from(value: DuplicateVoteEvidence) -> Self { - RawDuplicateVoteEvidence { + raw::DuplicateVoteEvidence { vote_a: Some(value.vote_a.into()), vote_b: Some(value.vote_b.into()), total_voting_power: value.total_voting_power.into(), @@ -188,9 +223,81 @@ tendermint_pb_modules! { } } - impl TryFrom for Data { + impl Protobuf for ConflictingBlock {} + + impl TryFrom for ConflictingBlock { + type Error = Error; + + fn try_from(value: raw::LightBlock) -> Result { + Ok(ConflictingBlock { + signed_header: value + .signed_header + .ok_or_else(Error::missing_evidence)? + .try_into()?, + validator_set: value + .validator_set + .ok_or_else(Error::missing_evidence)? + .try_into()?, + }) + } + } + + impl From for raw::LightBlock { + fn from(value: ConflictingBlock) -> Self { + raw::LightBlock { + signed_header: Some(value.signed_header.into()), + validator_set: Some(value.validator_set.into()), + } + } + } + + impl Protobuf for LightClientAttackEvidence {} + + impl TryFrom for LightClientAttackEvidence { + type Error = Error; + + fn try_from(ev: raw::LightClientAttackEvidence) -> Result { + Ok(LightClientAttackEvidence { + conflicting_block: ev + .conflicting_block + .ok_or_else(Error::missing_evidence)? + .try_into()?, + common_height: ev.common_height.try_into()?, + byzantine_validators: ev + .byzantine_validators + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + total_voting_power: ev.total_voting_power.try_into()?, + timestamp: ev + .timestamp + .ok_or_else(Error::missing_timestamp)? + .try_into()?, + }) + } + } + + impl From for raw::LightClientAttackEvidence { + fn from(ev: LightClientAttackEvidence) -> Self { + raw::LightClientAttackEvidence { + conflicting_block: Some(ev.conflicting_block.into()), + common_height: ev.common_height.into(), + byzantine_validators: ev + .byzantine_validators + .into_iter() + .map(Into::into) + .collect(), + total_voting_power: ev.total_voting_power.into(), + timestamp: Some(ev.timestamp.into()), + } + } + } + + impl Protobuf for List {} + + impl TryFrom for List { type Error = Error; - fn try_from(value: RawEvidenceList) -> Result { + fn try_from(value: raw::EvidenceList) -> Result { let evidence = value .evidence .into_iter() @@ -200,20 +307,20 @@ tendermint_pb_modules! { } } - impl From for RawEvidenceList { - fn from(value: Data) -> Self { - RawEvidenceList { + impl From for raw::EvidenceList { + fn from(value: List) -> Self { + raw::EvidenceList { evidence: value.0.into_iter().map(Into::into).collect(), } } } - impl Protobuf for Params {} + impl Protobuf for Params {} - impl TryFrom for Params { + impl TryFrom for Params { type Error = Error; - fn try_from(value: RawEvidenceParams) -> Result { + fn try_from(value: raw::EvidenceParams) -> Result { Ok(Self { max_age_num_blocks: value .max_age_num_blocks @@ -228,7 +335,7 @@ tendermint_pb_modules! { } } - impl From for RawEvidenceParams { + impl From for raw::EvidenceParams { fn from(value: Params) -> Self { Self { // Todo: Implement proper domain types so this becomes infallible diff --git a/tendermint/src/hash.rs b/tendermint/src/hash.rs index 93c313468..56f64736b 100644 --- a/tendermint/src/hash.rs +++ b/tendermint/src/hash.rs @@ -164,7 +164,6 @@ impl FromStr for Hash { } } -// Serialization is used in light-client config impl<'de> Deserialize<'de> for Hash { fn deserialize>(deserializer: D) -> Result { let hex = <&str>::deserialize(deserializer)?; diff --git a/tendermint/src/time.rs b/tendermint/src/time.rs index 443f5d7cd..90a0e47e3 100644 --- a/tendermint/src/time.rs +++ b/tendermint/src/time.rs @@ -141,6 +141,16 @@ impl Time { let t = self.0.checked_sub(duration)?; Self::from_utc(t.assume_utc()).ok() } + + /// Check whether this time is before the given time. + pub fn before(&self, other: Time) -> bool { + self.0.assume_utc() < other.0.assume_utc() + } + + /// Check whether this time is after the given time. + pub fn after(&self, other: Time) -> bool { + self.0.assume_utc() > other.0.assume_utc() + } } impl fmt::Display for Time { diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index 8b232736e..c4cd17c82 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -268,15 +268,6 @@ tendermint_pb_modules! { let proposer = value.proposer.map(TryInto::try_into).transpose()?; let validator_set = Self::new(validators, proposer); - // Ensure that the raw voting power matches the computed one - let raw_voting_power = value.total_voting_power.try_into()?; - if raw_voting_power != validator_set.total_voting_power() { - return Err(Error::raw_voting_power_mismatch( - raw_voting_power, - validator_set.total_voting_power(), - )); - } - Ok(validator_set) } } diff --git a/tools/kvstore-test/tests/light-client.rs b/tools/kvstore-test/tests/light-client.rs index d3980290e..d7be78e4e 100644 --- a/tools/kvstore-test/tests/light-client.rs +++ b/tools/kvstore-test/tests/light-client.rs @@ -14,14 +14,12 @@ use std::{convert::TryFrom, time::Duration}; -use tendermint::Hash; use tendermint_light_client::{ - builder::{LightClientBuilder, SupervisorBuilder}, - components::io::{AtHeight, Io, IoError, ProdIo}, + builder::LightClientBuilder, + components::io::{AtHeight, Io, ProdIo}, errors::Error, - evidence::{Evidence, EvidenceReporter}, + instance::Instance, store::{memory::MemoryStore, LightStore}, - supervisor::{Handle, Instance, Supervisor}, verifier::{ options::Options as LightClientOptions, types::{Height, PeerId, Status, TrustThreshold}, @@ -29,15 +27,6 @@ use tendermint_light_client::{ }; use tendermint_rpc as rpc; -struct TestEvidenceReporter; - -#[contracts::contract_trait] -impl EvidenceReporter for TestEvidenceReporter { - fn report(&self, evidence: Evidence, peer: PeerId) -> Result { - panic!("unexpected fork detected for peer {peer} with evidence: {evidence:?}"); - } -} - fn make_instance(peer_id: PeerId, options: LightClientOptions, address: rpc::Url) -> Instance { let rpc_client = rpc::HttpClient::new(address).unwrap(); let io = ProdIo::new(peer_id, rpc_client.clone(), Some(Duration::from_secs(2))); @@ -58,9 +47,8 @@ fn make_instance(peer_id: PeerId, options: LightClientOptions, address: rpc::Url .build() } -fn make_supervisor() -> Supervisor { +fn make_primary() -> Instance { let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(); - let witness: PeerId = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF".parse().unwrap(); // Because our CI infrastructure can only spawn a single Tendermint node at the moment, // we run this test against this very node as both the primary and witness. @@ -75,28 +63,19 @@ fn make_supervisor() -> Supervisor { clock_drift: Duration::from_secs(5 * 60), // 5 minutes }; - let primary_instance = make_instance(primary, options, node_address.clone()); - let witness_instance = make_instance(witness, options, node_address.clone()); - - SupervisorBuilder::new() - .primary(primary, node_address.clone(), primary_instance) - .witness(witness, node_address, witness_instance) - .build_prod() + make_instance(primary, options, node_address) } #[test] fn forward() { - let supervisor = make_supervisor(); - - let handle = supervisor.handle(); - std::thread::spawn(|| supervisor.run()); + let mut primary = make_primary(); let max_iterations: usize = 10; for i in 1..=max_iterations { println!("[info ] - iteration {i}/{max_iterations}"); - match handle.verify_to_highest() { + match primary.light_client.verify_to_highest(&mut primary.state) { Ok(light_block) => { println!("[info ] synced to block {}", light_block.height()); }, @@ -112,10 +91,7 @@ fn forward() { #[test] fn backward() -> Result<(), Error> { - let supervisor = make_supervisor(); - - let handle = supervisor.handle(); - std::thread::spawn(|| supervisor.run()); + let mut primary = make_primary(); let max_iterations: usize = 10; @@ -126,14 +102,17 @@ fn backward() -> Result<(), Error> { println!("[info ] - iteration {i}/{max_iterations}"); // First we sync to the highest block to have a high enough trusted state - let trusted_state = handle.verify_to_highest()?; + let trusted_state = primary.light_client.verify_to_highest(&mut primary.state)?; println!("[info ] synced to highest block {}", trusted_state.height()); // Then we pick a height below the trusted state let target_height = Height::try_from(trusted_state.height().value() / 2).unwrap(); // We now try to verify a block at this height - let light_block = handle.verify_to_target(target_height)?; + let light_block = primary + .light_client + .verify_to_target(target_height, &mut primary.state)?; + println!("[info ] verified lower block {}", light_block.height()); std::thread::sleep(Duration::from_millis(800)); diff --git a/tools/proto-compiler/src/constants.rs b/tools/proto-compiler/src/constants.rs index 0a8413844..7485089a9 100644 --- a/tools/proto-compiler/src/constants.rs +++ b/tools/proto-compiler/src/constants.rs @@ -42,6 +42,8 @@ const BASE64STRING: &str = r#"#[serde(with = "crate::serializers::bytes::base64s const VEC_BASE64STRING: &str = r#"#[serde(with = "crate::serializers::bytes::vec_base64string")]"#; const OPTIONAL: &str = r#"#[serde(with = "crate::serializers::optional")]"#; const BYTES_SKIP_IF_EMPTY: &str = r#"#[serde(skip_serializing_if = "bytes::Bytes::is_empty")]"#; +const SKIP: &str = "#[serde(skip)]"; +const RENAME_ALL_PASCALCASE: &str = r#"#[serde(rename_all = "PascalCase")]"#; const NULLABLEVECARRAY: &str = r#"#[serde(with = "crate::serializers::txs")]"#; const NULLABLE: &str = r#"#[serde(with = "crate::serializers::nullable")]"#; const ALIAS_POWER_QUOTED: &str = @@ -54,12 +56,12 @@ const RENAME_SRPUBKEY: &str = r#"#[serde(rename = "tendermint/PubKeySr25519", wi const RENAME_DUPLICATEVOTE: &str = r#"#[serde(rename = "tendermint/DuplicateVoteEvidence")]"#; const RENAME_LIGHTCLIENTATTACK: &str = r#"#[serde(rename = "tendermint/LightClientAttackEvidence")]"#; -const ALIAS_VALIDATOR_POWER_QUOTED: &str = - r#"#[serde(alias = "ValidatorPower", with = "crate::serializers::from_str")]"#; -const ALIAS_TOTAL_VOTING_POWER_QUOTED: &str = - r#"#[serde(alias = "TotalVotingPower", with = "crate::serializers::from_str")]"#; -const ALIAS_TIMESTAMP: &str = r#"#[serde(alias = "Timestamp")]"#; -const ALIAS_PARTS: &str = r#"#[serde(alias = "parts")]"#; +const RENAME_TOTAL_VOTING_POWER_QUOTED: &str = + r#"#[serde(rename = "TotalVotingPower", with = "crate::serializers::from_str")]"#; +const RENAME_VALIDATOR_POWER_QUOTED: &str = + r#"#[serde(rename = "ValidatorPower", with = "crate::serializers::from_str")]"#; +const RENAME_TIMESTAMP: &str = r#"#[serde(rename = "Timestamp")]"#; +const RENAME_PARTS: &str = r#"#[serde(rename = "parts", alias = "part_set_header")]"#; /// Custom type attributes applied on top of protobuf structs /// The first item in the tuple defines the message where the annotation should apply and @@ -68,10 +70,10 @@ const ALIAS_PARTS: &str = r#"#[serde(alias = "parts")]"#; /// https://docs.rs/prost-build/0.6.1/prost_build/struct.Config.html#method.btree_map pub static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[ (".tendermint.libs.bits.BitArray", SERIALIZED), - (".tendermint.types.EvidenceParams", SERIALIZED), (".tendermint.types.BlockIDFlag", PRIMITIVE_ENUM), (".tendermint.types.Block", SERIALIZED), (".tendermint.types.Data", SERIALIZED), + (".tendermint.types.EvidenceParams", SERIALIZED), (".tendermint.types.Evidence.sum", SERIALIZED), (".tendermint.types.Evidence.sum", TYPE_TAG), (".tendermint.types.EvidenceList", SERIALIZED), @@ -80,6 +82,10 @@ pub static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[ (".tendermint.types.BlockID", SERIALIZED), (".tendermint.types.PartSetHeader", SERIALIZED), (".tendermint.types.LightClientAttackEvidence", SERIALIZED), + ( + ".tendermint.types.LightClientAttackEvidence", + RENAME_ALL_PASCALCASE, + ), (".tendermint.types.LightBlock", SERIALIZED), (".tendermint.types.SignedHeader", SERIALIZED), (".tendermint.types.Header", SERIALIZED), @@ -87,7 +93,7 @@ pub static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[ (".tendermint.types.Commit", SERIALIZED), (".tendermint.types.CommitSig", SERIALIZED), (".tendermint.types.ValidatorSet", SERIALIZED), - (".tendermint.crypto.PublicKey", SERIALIZED), + (".tendermint.crypto.PublicKey.sum", SERIALIZED), (".tendermint.crypto.PublicKey.sum", TYPE_TAG), (".tendermint.abci.ResponseInfo", SERIALIZED), (".tendermint.types.CanonicalBlockID", SERIALIZED), @@ -109,6 +115,10 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[ ".tendermint.types.EvidenceParams.max_bytes", QUOTED_WITH_DEFAULT, ), + ( + ".tendermint.types.EvidenceParams.max_age_num_blocks", + QUOTED_WITH_DEFAULT, + ), (".tendermint.version.Consensus.block", QUOTED), (".tendermint.version.Consensus.app", QUOTED_WITH_DEFAULT), (".tendermint.abci.ResponseInfo.data", DEFAULT), @@ -127,11 +137,7 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[ BYTES_SKIP_IF_EMPTY, ), (".tendermint.types.BlockID.hash", HEXSTRING), - (".tendermint.types.BlockID.part_set_header", ALIAS_PARTS), - ( - ".tendermint.types.CanonicalBlockID.part_set_header", - ALIAS_PARTS, - ), + (".tendermint.types.BlockID.part_set_header", RENAME_PARTS), ( ".tendermint.types.PartSetHeader.total", PART_SET_HEADER_TOTAL, @@ -157,15 +163,23 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[ (".tendermint.types.CommitSig.signature", BASE64STRING), ( ".tendermint.types.DuplicateVoteEvidence.total_voting_power", - ALIAS_TOTAL_VOTING_POWER_QUOTED, + RENAME_TOTAL_VOTING_POWER_QUOTED, ), ( ".tendermint.types.DuplicateVoteEvidence.validator_power", - ALIAS_VALIDATOR_POWER_QUOTED, + RENAME_VALIDATOR_POWER_QUOTED, ), ( ".tendermint.types.DuplicateVoteEvidence.timestamp", - ALIAS_TIMESTAMP, + RENAME_TIMESTAMP, + ), + ( + ".tendermint.types.LightClientAttackEvidence.common_height", + QUOTED, + ), + ( + ".tendermint.types.LightClientAttackEvidence.total_voting_power", + QUOTED, ), (".tendermint.types.Vote.height", QUOTED), (".tendermint.types.Vote.validator_address", HEXSTRING), @@ -180,6 +194,7 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[ ".tendermint.types.Validator.proposer_priority", QUOTED_WITH_DEFAULT, ), // Default is for /genesis deserialization + (".tendermint.types.ValidatorSet.total_voting_power", SKIP), (".tendermint.types.BlockMeta.block_size", QUOTED), (".tendermint.types.BlockMeta.num_txs", QUOTED), (".tendermint.crypto.PublicKey.sum.ed25519", RENAME_EDPUBKEY), diff --git a/tools/proto-compiler/src/main.rs b/tools/proto-compiler/src/main.rs index 90d9f3df9..4991273a5 100644 --- a/tools/proto-compiler/src/main.rs +++ b/tools/proto-compiler/src/main.rs @@ -75,18 +75,18 @@ fn main() { // List available proto files let protos = find_proto_files(&proto_path); - let ver_target_dir = target_dir.join("prost").join(&version.ident); + let ver_target_dir = target_dir.join("prost").join(version.ident); let ver_module_dir = target_dir.join("tendermint"); let out_dir = var("OUT_DIR") - .map(|d| Path::new(&d).join(&version.ident)) + .map(|d| Path::new(&d).join(version.ident)) .or_else(|_| tempdir().map(|d| d.into_path())) .unwrap(); let mut pb = prost_build::Config::new(); // Use shared Bytes buffers for ABCI messages: - pb.bytes(&[".tendermint.abci"]); + pb.bytes([".tendermint.abci"]); // Compile proto files with added annotations, exchange prost_types to our own pb.out_dir(&out_dir); @@ -121,7 +121,7 @@ fn main() { ver_target_dir.to_string_lossy(), ); copy_files(&out_dir, &ver_target_dir); // This panics if it fails. - generate_tendermint_mod(&out_dir, &version, &ver_module_dir); + generate_tendermint_mod(&out_dir, version, &ver_module_dir); } generate_tendermint_lib(TENDERMINT_VERSIONS, &target_dir.join("tendermint.rs"));