#![cfg(feature = "unstable")] use std::{collections::HashMap, time::Duration}; use proptest::{prelude::*, test_runner::TestRng}; use tendermint::{hash::Algorithm, Hash, Time}; use tendermint_light_client::{ components::{ io::{AtHeight, Io}, scheduler, }, errors::Error, light_client::LightClient, state::State, store::{memory::MemoryStore, LightStore}, tests::{MockClock, MockIo}, verifier::{ options::Options, types::{Height, LightBlock, Status}, ProdVerifier, }, }; use tendermint_testgen::{ light_block::{default_peer_id, TmLightBlock as TGLightBlock}, Generator, LightChain, }; 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 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).unwrap(), }; 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(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 light_client = LightClient::new( primary, options, clock, scheduler::basic_bisecting_schedule, verifier, 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(Time::from_unix_timestamp(1610105021, 0).unwrap()); } 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: 100, ..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)?; } }