diff --git a/.gitignore b/.gitignore index 42a22f676..46c3ebed2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ Cargo.lock # RPC probe results /rpc-probe/probe-results/ + +# Proptest regressions dumps +**/*.proptest-regressions diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b26f75d7..de6172711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## FEATURES + +* `[light-client]` Add basic support for backward verification, behind a `unstable` feature flag. ([#361]) + Note: This feature is currently unstable and should not be relied on by downstream dependencies. + ## IMPROVEMENTS: * `[all]` Update all crates to use the latest version of the following dependencies: ([#764]) @@ -16,9 +21,10 @@ * `[light-client]` The `sled`-backed lightstore is now feature-guarded under the `lightstore-sled` feature, which is enabled by default for now. ([#428]) -[#769]: https://github.com/informalsystems/tendermint-rs/issues/769 -[#764]: https://github.com/informalsystems/tendermint-rs/issues/764 +[#361]: https://github.com/informalsystems/tendermint-rs/issues/361 [#428]: https://github.com/informalsystems/tendermint-rs/issues/428 +[#764]: https://github.com/informalsystems/tendermint-rs/issues/764 +[#769]: https://github.com/informalsystems/tendermint-rs/issues/769 ## v0.17.1 diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 3440c95c7..c68460048 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -33,6 +33,7 @@ default = ["rpc-client", "lightstore-sled"] rpc-client = ["tokio", "tendermint-rpc/http-client"] secp256k1 = ["tendermint/secp256k1", "tendermint-rpc/secp256k1"] lightstore-sled = ["sled"] +unstable = [] [dependencies] tendermint = { version = "0.17.1", path = "../tendermint" } @@ -58,3 +59,4 @@ serde_json = "1.0.51" gumdrop = "0.8.0" rand = "0.7.3" tempdir = "0.3.7" +proptest = "0.10.1" diff --git a/light-client/src/builder/light_client.rs b/light-client/src/builder/light_client.rs index 1d4f5b36d..eea8d5631 100644 --- a/light-client/src/builder/light_client.rs +++ b/light-client/src/builder/light_client.rs @@ -208,6 +208,7 @@ impl LightClientBuilder { self.clock, self.scheduler, self.verifier, + self.hasher, self.io, ); diff --git a/light-client/src/errors.rs b/light-client/src/errors.rs index c2bc69d98..00c69d78c 100644 --- a/light-client/src/errors.rs +++ b/light-client/src/errors.rs @@ -11,7 +11,7 @@ use crate::{ components::io::IoError, light_client::Options, predicates::errors::VerificationError, - types::{Height, LightBlock, PeerId, Status}, + types::{Hash, Height, LightBlock, PeerId, Status}, }; /// An error raised by this library @@ -78,6 +78,19 @@ pub enum ErrorKind { #[error("invalid light block: {0}")] InvalidLightBlock(#[source] VerificationError), + /// Hash mismatch between two adjacent headers + #[error("hash mismatch between two adjacent headers: {h1} != {h2}")] + InvalidAdjacentHeaders { + /// Hash #1 + h1: Hash, + /// Hash #2 + h2: Hash, + }, + + /// Missing last_block_id field for header at given height + #[error("missing last_block_id for header at height {0}")] + MissingLastBlockId(Height), + /// Internal channel disconnected #[error("internal channel disconnected")] ChannelDisconnected, diff --git a/light-client/src/light_client.rs b/light-client/src/light_client.rs index 3413f2f39..1ade39a89 100644 --- a/light-client/src/light_client.rs +++ b/light-client/src/light_client.rs @@ -2,23 +2,23 @@ //! //! [1]: https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification/verification.md +use std::{fmt, time::Duration}; + use contracts::*; use derive_more::Display; use serde::{Deserialize, Serialize}; -use std::{fmt, time::Duration}; -use crate::components::{clock::Clock, io::*, scheduler::*, verifier::*}; -use crate::contracts::*; use crate::{ bail, + components::{clock::Clock, io::*, scheduler::*, verifier::*}, + contracts::*, errors::{Error, ErrorKind}, + operations::Hasher, state::State, types::{Height, LightBlock, PeerId, Status, TrustThreshold}, }; /// Verification parameters -/// -/// TODO: Find a better name than `Options` #[derive(Copy, Clone, Debug, PartialEq, Display, Serialize, Deserialize)] #[display(fmt = "{:?}", self)] pub struct Options { @@ -53,10 +53,15 @@ pub struct LightClient { pub peer: PeerId, /// Options for this light client pub options: Options, + clock: Box, scheduler: Box, verifier: Box, io: Box, + + // Only used in verify_backwards when "unstable" feature is enabled + #[allow(dead_code)] + hasher: Box, } impl fmt::Debug for LightClient { @@ -76,6 +81,7 @@ impl LightClient { clock: impl Clock + 'static, scheduler: impl Scheduler + 'static, verifier: impl Verifier + 'static, + hasher: impl Hasher + 'static, io: impl Io + 'static, ) -> Self { Self { @@ -84,6 +90,7 @@ impl LightClient { clock: Box::new(clock), scheduler: Box::new(scheduler), verifier: Box::new(verifier), + hasher: Box::new(hasher), io: Box::new(io), } } @@ -95,6 +102,7 @@ impl LightClient { clock: Box, scheduler: Box, verifier: Box, + hasher: Box, io: Box, ) -> Self { Self { @@ -103,6 +111,7 @@ impl LightClient { clock, scheduler, verifier, + hasher, io, } } @@ -127,8 +136,10 @@ impl LightClient { /// communicates with other nodes. /// - The Verifier component checks whether a header is valid and checks if a new light block /// should be trusted based on a previously verified light block. - /// - The Scheduler component decides which height to try to verify next, in case the current - /// block pass verification but cannot be trusted yet. + /// - When doing _forward_ verification, the Scheduler component decides which height to try to + /// verify next, in case the current block pass verification but cannot be trusted yet. + /// - When doing _backward_ verification, the Hasher component is used to determine + /// whether the `last_block_id` hash of a block matches the hash of the block right below it. /// /// ## Implements /// - [LCV-DIST-SAFE.1] @@ -158,12 +169,33 @@ impl LightClient { target_height: Height, state: &mut State, ) -> Result { - // Let's first look in the store to see whether we have already successfully verified this - // block. + // Let's first look in the store to see whether + // we have already successfully verified this block. if let Some(light_block) = state.light_store.get_trusted_or_verified(target_height) { return Ok(light_block); } + // Get the highest trusted state + let highest = state + .light_store + .highest_trusted_or_verified() + .ok_or(ErrorKind::NoInitialTrustedState)?; + + if target_height >= highest.height() { + // Perform forward verification with bisection + self.verify_forward(target_height, state) + } else { + // Perform sequential backward verification + self.verify_backward(target_height, state) + } + } + + /// Perform forward verification with bisection. + fn verify_forward( + &self, + target_height: Height, + state: &mut State, + ) -> Result { let mut current_height = target_height; loop { @@ -239,6 +271,108 @@ impl LightClient { } } + /// Stub for when "unstable" feature is disabled. + #[doc(hidden)] + #[cfg(not(feature = "unstable"))] + fn verify_backward( + &self, + target_height: Height, + state: &mut State, + ) -> Result { + let trusted_state = state + .light_store + .highest_trusted_or_verified() + .ok_or(ErrorKind::NoInitialTrustedState)?; + + Err(ErrorKind::TargetLowerThanTrustedState { + target_height, + trusted_height: trusted_state.height(), + } + .into()) + } + + /// Perform sequential backward verification. + /// + /// Backward verification is implemented by taking a sliding window + /// of length two between the trusted state and the target block and + /// checking whether the last_block_id hash of the higher block + /// matches the computed hash of the lower block. + /// + /// ## Performance + /// The algorithm implemented is very inefficient in case the target + /// block is much lower than the highest trusted state. + /// For a trusted state at height `T`, and a target block at height `H`, + /// it will fetch and check hashes of `T - H` blocks. + /// + /// ## Stability + /// This feature is only available if the `unstable` flag of is enabled. + /// If the flag is disabled, then any attempt to verify a block whose + /// height is lower than the highest trusted state will result in a + /// `TargetLowerThanTrustedState` error. + #[cfg(feature = "unstable")] + fn verify_backward( + &self, + target_height: Height, + state: &mut State, + ) -> Result { + use std::convert::TryFrom; + + let root = state + .light_store + .highest_trusted_or_verified() + .ok_or(ErrorKind::NoInitialTrustedState)?; + + assert!(root.height() >= target_height); + + // Check invariant [LCV-INV-TP.1] + if !is_within_trust_period(&root, self.options.trusting_period, self.clock.now()) { + bail!(ErrorKind::TrustedStateOutsideTrustingPeriod { + trusted_state: Box::new(root), + options: self.options, + }); + } + + // Compute a range of `Height`s from `trusted_height - 1` to `target_height`, inclusive. + let range = (target_height.value()..root.height().value()).rev(); + let heights = range.map(|h| Height::try_from(h).unwrap()); + + let mut latest = root; + + for height in heights { + let (current, _status) = self.get_or_fetch_block(height, state)?; + + let latest_last_block_id = latest + .signed_header + .header + .last_block_id + .ok_or_else(|| ErrorKind::MissingLastBlockId(latest.height()))?; + + let current_hash = self.hasher.hash_header(¤t.signed_header.header); + + if current_hash != latest_last_block_id.hash { + bail!(ErrorKind::InvalidAdjacentHeaders { + h1: current_hash, + h2: latest_last_block_id.hash + }); + } + + // `latest` and `current` are linked together by `last_block_id`, + // therefore it is not relevant which we verified first. + // For consistency, we say that `latest` was verifed using + // `current` so that the trace is always pointing down the chain. + state.light_store.insert(current.clone(), Status::Trusted); + state.light_store.insert(latest.clone(), Status::Trusted); + state.trace_block(latest.height(), current.height()); + + latest = current; + } + + // We reached the target height. + assert_eq!(latest.height(), target_height); + + Ok(latest) + } + /// Look in the light store for a block from the given peer at the given height, /// which has not previously failed verification (ie. its status is not `Failed`). /// diff --git a/light-client/src/store.rs b/light-client/src/store.rs index 6a058de04..d7c99ebf3 100644 --- a/light-client/src/store.rs +++ b/light-client/src/store.rs @@ -76,10 +76,10 @@ pub trait LightStore: Debug + Send + Sync { /// Get the light block of lowest height with the trusted or verified status. fn lowest_trusted_or_verified(&self) -> Option { - let latest_trusted = self.lowest(Status::Trusted); - let latest_verified = self.lowest(Status::Verified); + let lowest_trusted = self.lowest(Status::Trusted); + let lowest_verified = self.lowest(Status::Verified); - std_ext::option::select(latest_trusted, latest_verified, |t, v| { + std_ext::option::select(lowest_trusted, lowest_verified, |t, v| { std_ext::cmp::min_by_key(t, v, |lb| lb.height()) }) } diff --git a/light-client/src/tests.rs b/light-client/src/tests.rs index 50c9f4e03..9cf6e30ca 100644 --- a/light-client/src/tests.rs +++ b/light-client/src/tests.rs @@ -42,7 +42,7 @@ pub struct Initial { } #[derive(Deserialize, Clone, Debug)] -pub struct TestBisection { +pub struct LightClientTest { pub description: String, pub trust_options: TrustOptions, pub primary: Provider, diff --git a/light-client/src/utils/std_ext.rs b/light-client/src/utils/std_ext.rs index 40598b386..4a9473cbf 100644 --- a/light-client/src/utils/std_ext.rs +++ b/light-client/src/utils/std_ext.rs @@ -2,7 +2,7 @@ pub mod cmp { /// Stable version of `std::cmp::max_by_key`. pub fn max_by_key(a: A, b: A, key: impl Fn(&A) -> B) -> A { - if key(&a) >= key(&b) { + if key(&a) > key(&b) { a } else { b diff --git a/light-client/tests/backward.rs b/light-client/tests/backward.rs new file mode 100644 index 000000000..d77747780 --- /dev/null +++ b/light-client/tests/backward.rs @@ -0,0 +1,262 @@ +#![cfg(feature = "unstable")] + +use std::{collections::HashMap, time::Duration}; + +use tendermint::{hash::Algorithm, Hash}; + +use tendermint_light_client::{ + components::{ + io::{AtHeight, Io}, + scheduler, + verifier::ProdVerifier, + }, + errors::Error, + light_client::{LightClient, Options}, + operations::ProdHasher, + state::State, + store::{memory::MemoryStore, LightStore}, + tests::{MockClock, MockIo}, + types::{Height, LightBlock, Status}, +}; + +use tendermint_testgen::{ + light_block::{default_peer_id, TMLightBlock as TGLightBlock}, + Generator, LightChain, +}; + +use proptest::{prelude::*, test_runner::TestRng}; + +fn testgen_to_lb(tm_lb: TGLightBlock) -> LightBlock { + LightBlock { + signed_header: tm_lb.signed_header, + validators: tm_lb.validators, + next_validators: tm_lb.next_validators, + provider: tm_lb.provider, + } +} + +#[derive(Clone, Debug)] +struct TestCase { + length: u32, + chain: LightChain, + target_height: Height, + trusted_height: Height, +} + +fn make(chain: LightChain, trusted_height: Height) -> (LightClient, State) { + let primary = default_peer_id(); + let chain_id = "testchain-1".parse().unwrap(); + + let clock = MockClock { + /// Set the current time to be ahead of the latest block in the chain + now: tendermint_testgen::helpers::get_time(chain.light_blocks.len() as u64 + 1), + }; + + let options = Options { + trust_threshold: Default::default(), + trusting_period: Duration::from_secs(60 * 60 * 24 * 10), + clock_drift: Duration::from_secs(10), + }; + + let light_blocks = chain + .light_blocks + .into_iter() + .map(|lb| lb.generate().unwrap()) + .map(testgen_to_lb) + .collect(); + + let io = MockIo::new(chain_id, light_blocks); + + let trusted_state = io + .fetch_light_block(AtHeight::At(trusted_height)) + .expect("could not find trusted light block"); + + let mut light_store = MemoryStore::new(); + light_store.insert(trusted_state, Status::Trusted); + + let state = State { + light_store: Box::new(light_store), + verification_trace: HashMap::new(), + }; + + let verifier = ProdVerifier::default(); + let hasher = ProdHasher::default(); + + let light_client = LightClient::new( + primary, + options, + clock, + scheduler::basic_bisecting_schedule, + verifier, + hasher, + io, + ); + + (light_client, state) +} + +fn verify(tc: TestCase) -> Result { + let (light_client, mut state) = make(tc.chain, tc.trusted_height); + light_client.verify_to_target(tc.target_height, &mut state) +} + +fn ok_test(tc: TestCase) -> Result<(), TestCaseError> { + let target_height = tc.target_height; + let result = verify(tc); + + prop_assert_eq!(result.unwrap().height(), target_height); + + Ok(()) +} + +fn bad_test(tc: TestCase) -> Result<(), TestCaseError> { + let result = verify(tc); + prop_assert!(result.is_err()); + Ok(()) +} + +fn testcase(max: u32) -> impl Strategy { + (1..=max).prop_flat_map(move |length| { + (1..=length).prop_flat_map(move |trusted_height| { + (1..=trusted_height).prop_map(move |target_height| TestCase { + chain: LightChain::default_with_length(length as u64), + length, + trusted_height: trusted_height.into(), + target_height: target_height.into(), + }) + }) + }) +} + +fn remove_last_block_id_hash(mut tc: TestCase, mut rng: TestRng) -> TestCase { + let from = tc.target_height.value() + 1; + let to = tc.trusted_height.value() + 1; + let height = rng.gen_range(from, to); + + dbg!(tc.target_height, tc.trusted_height, height); + + let block = tc.chain.block_mut(height).unwrap(); + + if let Some(header) = block.header.as_mut() { + header.last_block_id_hash = None; + } + + tc +} + +fn corrupt_hash(mut tc: TestCase, mut rng: TestRng) -> TestCase { + let from = tc.target_height.value(); + let to = tc.trusted_height.value(); + let height = rng.gen_range(from, to); + + dbg!(tc.target_height, tc.trusted_height, height); + + let block = tc.chain.block_mut(height).unwrap(); + + if let Some(header) = block.header.as_mut() { + header.time = Some(1610105021); + } + + tc +} + +fn corrupt_last_block_id_hash(mut tc: TestCase, mut rng: TestRng) -> TestCase { + let from = tc.target_height.value() + 1; + let to = tc.trusted_height.value() + 1; + let height = rng.gen_range(from, to); + + dbg!(tc.target_height, tc.trusted_height, height); + + let block = tc.chain.block_mut(height).unwrap(); + + if let Some(header) = block.header.as_mut() { + let hash = Hash::from_hex_upper( + Algorithm::Sha256, + "C68B4CFC7F9AA239F9E0DF7CDEF264DD1CDFE8B73EF04B5600A20111144F42BF", + ) + .unwrap(); + + header.last_block_id_hash = Some(hash); + } + + tc +} + +fn tc_missing_last_block_id_hash(max: u32) -> impl Strategy { + testcase(max) + .prop_filter("target == trusted", |tc| { + tc.target_height != tc.trusted_height + }) + .prop_perturb(remove_last_block_id_hash) +} + +fn tc_corrupted_last_block_id_hash(max: u32) -> impl Strategy { + testcase(max) + .prop_filter("target == trusted", |tc| { + tc.target_height != tc.trusted_height + }) + .prop_perturb(corrupt_last_block_id_hash) +} + +fn tc_corrupted_hash(max: u32) -> impl Strategy { + testcase(max) + .prop_filter("target == trusted", |tc| { + tc.target_height != tc.trusted_height + }) + .prop_perturb(corrupt_hash) +} + +proptest! { + #![proptest_config(ProptestConfig { + cases: 20, + max_shrink_iters: 0, + ..Default::default() + })] + + #[test] + fn prop_target_equal_trusted_first_block(mut tc in testcase(100)) { + tc.target_height = 1_u32.into(); + tc.trusted_height = 1_u32.into(); + ok_test(tc)?; + } + + #[test] + fn prop_target_equal_trusted_last_block(mut tc in testcase(100)) { + tc.target_height = tc.length.into(); + tc.trusted_height = tc.length.into(); + ok_test(tc)?; + } + + #[test] + fn prop_target_equal_trusted(mut tc in testcase(100)) { + tc.target_height = tc.trusted_height; + ok_test(tc)?; + } + + #[test] + fn prop_two_ends(mut tc in testcase(100)) { + tc.target_height = 1_u32.into(); + tc.trusted_height = tc.length.into(); + ok_test(tc)?; + } + + #[test] + fn prop_target_less_than_trusted(tc in testcase(100)) { + ok_test(tc)?; + } + + #[test] + fn missing_last_block_id_hash(tc in tc_missing_last_block_id_hash(100)) { + bad_test(tc)?; + } + + #[test] + fn corrupted_last_block_id_hash(tc in tc_corrupted_last_block_id_hash(100)) { + bad_test(tc)?; + } + + #[test] + fn corrupted_hash(tc in tc_corrupted_hash(100)) { + bad_test(tc)?; + } +} diff --git a/light-client/tests/light_client.rs b/light-client/tests/light_client.rs index f33639cc7..ed69765f5 100644 --- a/light-client/tests/light_client.rs +++ b/light-client/tests/light_client.rs @@ -7,15 +7,15 @@ use tendermint_light_client::{ scheduler, verifier::ProdVerifier, }, - errors::{Error, ErrorKind}, + errors::Error, light_client::{LightClient, Options}, + operations::ProdHasher, state::State, store::{memory::MemoryStore, LightStore}, tests::*, types::{LightBlock, Status}, }; -use std::convert::TryInto; use tendermint_testgen::light_block::default_peer_id; use tendermint_testgen::Tester; @@ -28,7 +28,7 @@ struct BisectionTestResult { new_states: Result, Error>, } -fn run_bisection_test(tc: TestBisection) -> BisectionTestResult { +fn run_test(tc: LightClientTest) -> BisectionTestResult { let primary = default_peer_id(); let untrusted_height = tc.height_to_verify; let trust_threshold = tc.trust_options.trust_level; @@ -64,6 +64,7 @@ fn run_bisection_test(tc: TestBisection) -> BisectionTestResult { }; let verifier = ProdVerifier::default(); + let hasher = ProdHasher::default(); let mut light_client = LightClient::new( primary, @@ -71,6 +72,7 @@ fn run_bisection_test(tc: TestBisection) -> BisectionTestResult { clock, scheduler::basic_bisecting_schedule, verifier, + hasher, io.clone(), ); @@ -86,13 +88,13 @@ fn run_bisection_test(tc: TestBisection) -> BisectionTestResult { } } -fn bisection_test(tc: TestBisection) { +fn forward_test(tc: LightClientTest) { let expect_error = match &tc.expected_output { Some(eo) => eo.eq("error"), None => false, }; - let test_result = run_bisection_test(tc); + let test_result = run_test(tc); let expected_state = test_result.untrusted_light_block; match test_result.new_states { @@ -110,42 +112,10 @@ fn bisection_test(tc: TestBisection) { } } -/// Test that the light client fails with `ErrorKind::TargetLowerThanTrustedState` -/// when the target height is lower than the last trusted state height. -/// -/// To do this, we override increment the trusted height by 1 -/// and set the target height to `trusted_height - 1`, then run -/// the bisection test as normal. We then assert that we get the expected error. -fn bisection_lower_test(mut tc: TestBisection) { - let mut trusted_height = tc.trust_options.height; - - if trusted_height.value() <= 1 { - tc.trust_options.height = trusted_height.increment(); - trusted_height = trusted_height.increment(); - } - - tc.height_to_verify = (trusted_height.value() - 1).try_into().unwrap(); - - let test_result = run_bisection_test(tc); - match test_result.new_states { - Ok(_) => { - panic!("test unexpectedly succeeded, expected TargetLowerThanTrustedState error"); - } - Err(e) => match e.kind() { - ErrorKind::TargetLowerThanTrustedState { .. } => (), - kind => panic!( - "unexpected error, expected: TargetLowerThanTrustedState, got: {}", - kind - ), - }, - } -} - #[test] -fn run_bisection_tests() { - let mut tester = Tester::new("bisection", TEST_FILES_PATH); - tester.add_test("bisection test", bisection_test); - tester.add_test("bisection lower test", bisection_lower_test); +fn run_tests() { + let mut tester = Tester::new("light client verification", TEST_FILES_PATH); + tester.add_test("forward verification with bisection", forward_test); tester.run_foreach_in_dir("bisection/single_peer"); tester.finalize(); } diff --git a/light-client/tests/supervisor.rs b/light-client/tests/supervisor.rs index 54e5cfab8..4186414d7 100644 --- a/light-client/tests/supervisor.rs +++ b/light-client/tests/supervisor.rs @@ -6,6 +6,7 @@ use tendermint_light_client::{ }, fork_detector::ProdForkDetector, light_client::{self, LightClient}, + operations::ProdHasher, peer_list::PeerList, state::State, store::LightStore, @@ -18,7 +19,7 @@ use std::time::Duration; use tendermint_light_client::store::memory::MemoryStore; use tendermint_light_client::tests::{ - MockClock, MockEvidenceReporter, MockIo, TestBisection, TrustOptions, + LightClientTest, MockClock, MockEvidenceReporter, MockIo, TrustOptions, }; use tendermint_testgen::Tester; @@ -45,16 +46,17 @@ fn make_instance(peer_id: PeerId, trust_options: TrustOptions, io: MockIo, now: clock_drift: Duration::from_secs(10), }; - let verifier = ProdVerifier::default(); let clock = MockClock { now }; + let verifier = ProdVerifier::default(); + let hasher = ProdHasher::default(); let scheduler = scheduler::basic_bisecting_schedule; - let light_client = LightClient::new(peer_id, options, clock, scheduler, verifier, io); + let light_client = LightClient::new(peer_id, options, clock, scheduler, verifier, hasher, io); Instance::new(light_client, state) } -fn run_multipeer_test(tc: TestBisection) { +fn run_multipeer_test(tc: LightClientTest) { let primary = tc.primary.lite_blocks[0].provider; println!( diff --git a/testgen/src/light_chain.rs b/testgen/src/light_chain.rs index 7b21af03d..1ea9bf4b3 100644 --- a/testgen/src/light_chain.rs +++ b/testgen/src/light_chain.rs @@ -4,31 +4,12 @@ use tendermint::chain::Info; use std::convert::{TryFrom, TryInto}; +#[derive(Clone, Debug)] pub struct LightChain { pub info: Info, pub light_blocks: Vec, } -impl Default for LightChain { - fn default() -> Self { - let initial_block = LightBlock::new_default(1); - - let id = initial_block.chain_id().parse().unwrap(); - let height = initial_block.height().try_into().unwrap(); - - let info = Info { - id, - height, - // no last block id for the initial block - last_block_id: None, - // TODO: Not sure yet what this time means - time: None, - }; - - Self::new(info, vec![initial_block]) - } -} - impl LightChain { pub fn new(info: Info, light_blocks: Vec) -> Self { LightChain { info, light_blocks } @@ -37,11 +18,32 @@ impl LightChain { // TODO: make this fn more usable // TODO: like how does someone generate a chain with different validators at each height pub fn default_with_length(num: u64) -> Self { - let mut light_chain = Self::default(); + let mut last_block = LightBlock::new_default(1); + let mut light_blocks: Vec = vec![last_block.clone()]; + for _i in 2..=num { - light_chain.advance_chain(); + // add "next" light block to the vector + last_block = last_block.next(); + light_blocks.push(last_block.clone()); } - light_chain + + let id = last_block.chain_id().parse().unwrap(); + let height = last_block.height().try_into().unwrap(); + let last_block_hash = last_block.header.map(|h| h.generate().unwrap().hash()); + let last_block_id = last_block_hash.map(|hash| block::Id { + hash, + part_set_header: Default::default(), + }); + + let info = Info { + id, + height, + last_block_id, + // TODO: Not sure yet what this time means + time: None, + }; + + Self::new(info, light_blocks) } /// expects at least one LightBlock in the Chain @@ -81,6 +83,14 @@ impl LightChain { .find(|lb| lb.height() == target_height) } + /// fetches a mutable block from LightChain at a certain height + /// it returns None if a block does not exist for the target_height + pub fn block_mut(&mut self, target_height: u64) -> Option<&mut LightBlock> { + self.light_blocks + .iter_mut() + .find(|lb| lb.height() == target_height) + } + /// fetches the latest block from LightChain pub fn latest_block(&self) -> &LightBlock { self.light_blocks diff --git a/tools/kvstore-test/Cargo.toml b/tools/kvstore-test/Cargo.toml index f2d12e1c7..eebb5144c 100644 --- a/tools/kvstore-test/Cargo.toml +++ b/tools/kvstore-test/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" [dev-dependencies] futures = "0.3" tendermint = { version = "0.17.1", path = "../../tendermint" } -tendermint-light-client = { version = "0.17.1", path = "../../light-client" } +tendermint-light-client = { version = "0.17.1", path = "../../light-client", features = ["unstable"] } tendermint-rpc = { version = "0.17.1", path = "../../rpc", features = [ "http-client", "websocket-client" ] } tokio = { version = "1.0", features = [ "rt-multi-thread", "macros" ] } contracts = "0.4.0" diff --git a/tools/kvstore-test/tests/light-client.rs b/tools/kvstore-test/tests/light-client.rs index 7d1e6c94d..b82a86f35 100644 --- a/tools/kvstore-test/tests/light-client.rs +++ b/tools/kvstore-test/tests/light-client.rs @@ -1,39 +1,48 @@ //! Light Client integration tests. //! -/// If you have a kvstore app running on 127.0.0.1:26657, -/// these can be run using: -/// -/// cargo test -/// -/// Or else, if you have docker installed, you can tell the tests to run an endpoint, -/// by running: -/// -/// cargo make -/// -/// (Make sure you install cargo-make using `cargo install cargo-make` first.) -/// +//! If you have a kvstore app running on 127.0.0.1:26657, +//! these can be run using: +//! +//! cargo test +//! +//! Or else, if you have docker installed, you can tell the tests to run an endpoint, +//! by running: +//! +//! cargo make +//! +//! (Make sure you install cargo-make using `cargo install cargo-make` first.) +//! use tendermint_light_client::{ - builder::LightClientBuilder, - builder::SupervisorBuilder, - components::io::AtHeight, - components::io::Io, - components::io::IoError, - components::io::ProdIo, + builder::{LightClientBuilder, SupervisorBuilder}, + components::io::{AtHeight, Io, IoError, ProdIo}, + errors::Error, evidence::{Evidence, EvidenceReporter}, light_client, - store::memory::MemoryStore, - store::LightStore, - supervisor::{Handle, Instance}, - types::{PeerId, Status, TrustThreshold}, + store::{memory::MemoryStore, LightStore}, + supervisor::{Handle, Instance, Supervisor}, + types::{Height, PeerId, Status, TrustThreshold}, }; use tendermint::abci::transaction::Hash as TxHash; use tendermint::net; use tendermint_rpc as rpc; +use std::convert::TryFrom; use std::time::Duration; +struct TestEvidenceReporter; + +#[contracts::contract_trait] +impl EvidenceReporter for TestEvidenceReporter { + fn report(&self, evidence: Evidence, peer: PeerId) -> Result { + panic!( + "unexpected fork detected for peer {} with evidence: {:?}", + peer, evidence + ); + } +} + fn make_instance( peer_id: PeerId, options: light_client::Options, @@ -58,20 +67,7 @@ fn make_instance( .build() } -struct TestEvidenceReporter; - -#[contracts::contract_trait] -impl EvidenceReporter for TestEvidenceReporter { - fn report(&self, evidence: Evidence, peer: PeerId) -> Result { - panic!( - "unexpected fork detected for peer {} with evidence: {:?}", - peer, evidence - ); - } -} - -#[test] -fn sync() { +fn make_supervisor() -> Supervisor { let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(); let witness: PeerId = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF".parse().unwrap(); @@ -99,6 +95,13 @@ fn sync() { .witness(witness, node_address, witness_instance) .build_prod(); + supervisor +} + +#[test] +fn forward() { + let supervisor = make_supervisor(); + let handle = supervisor.handle(); std::thread::spawn(|| supervisor.run()); @@ -120,3 +123,35 @@ fn sync() { std::thread::sleep(Duration::from_millis(800)); } } + +#[test] +fn backward() -> Result<(), Error> { + let supervisor = make_supervisor(); + + let handle = supervisor.handle(); + std::thread::spawn(|| supervisor.run()); + + let max_iterations: usize = 10; + + // Sleep a little bit to ensure we have a few blocks already + std::thread::sleep(Duration::from_secs(2)); + + for i in 1..=max_iterations { + 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()?; + 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)?; + println!("[info ] verified lower block {}", light_block.height()); + + std::thread::sleep(Duration::from_millis(800)); + } + + Ok(()) +}