Skip to content

Commit f776dac

Browse files
authored
light-client: Fix verification of blocks between two trusted heights (informalsystems#1247)
1 parent d28b935 commit f776dac

File tree

10 files changed

+151
-14
lines changed

10 files changed

+151
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- `[tendermint-light-client]` Fix verification of blocks between two trusted
2+
heights
3+
([#1246](https://github.com/informalsystems/tendermint-rs/issues/1246))

.github/workflows/test.yml

+7
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,14 @@ jobs:
8181
with:
8282
toolchain: stable
8383
override: true
84+
# NOTE: We test with default features to make sure things work without "unstable".
8485
- uses: actions-rs/cargo@v1
86+
name: Test with default features
87+
with:
88+
command: test
89+
args: -p tendermint-light-client
90+
- uses: actions-rs/cargo@v1
91+
name: Test with all features
8592
with:
8693
command: test-all-features
8794
args: -p tendermint-light-client

light-client/src/components/scheduler.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub trait Scheduler: Send + Sync {
2020
///
2121
/// ## Postcondition
2222
/// - The resulting height must be valid according to `valid_schedule`. [LCV-SCHEDULE-POST.1]
23-
#[requires(light_store.highest_trusted_or_verified().is_some())]
23+
#[requires(light_store.highest_trusted_or_verified_before(target_height).is_some())]
2424
#[ensures(valid_schedule(ret, target_height, current_height, light_store))]
2525
fn schedule(
2626
&self,
@@ -61,7 +61,7 @@ pub fn basic_bisecting_schedule(
6161
target_height: Height,
6262
) -> Height {
6363
let trusted_height = light_store
64-
.highest_trusted_or_verified()
64+
.highest_trusted_or_verified_before(target_height)
6565
.map(|lb| lb.height())
6666
.unwrap();
6767

@@ -105,7 +105,7 @@ pub fn valid_schedule(
105105
light_store: &dyn LightStore,
106106
) -> bool {
107107
let latest_trusted_height = light_store
108-
.highest_trusted_or_verified()
108+
.highest_trusted_or_verified_before(target_height)
109109
.map(|lb| lb.height())
110110
.unwrap();
111111

light-client/src/light_client.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ impl LightClient {
161161
// Get the highest trusted state
162162
let highest = state
163163
.light_store
164-
.highest_trusted_or_verified()
164+
.highest_trusted_or_verified_before(target_height)
165+
.or_else(|| state.light_store.lowest_trusted_or_verified())
165166
.ok_or_else(Error::no_initial_trusted_state)?;
166167

167168
if target_height >= highest.height() {
@@ -187,7 +188,7 @@ impl LightClient {
187188
// Get the latest trusted state
188189
let trusted_block = state
189190
.light_store
190-
.highest_trusted_or_verified()
191+
.highest_trusted_or_verified_before(target_height)
191192
.ok_or_else(Error::no_initial_trusted_state)?;
192193

193194
if target_height < trusted_block.height() {
@@ -267,7 +268,8 @@ impl LightClient {
267268
) -> Result<LightBlock, Error> {
268269
let trusted_state = state
269270
.light_store
270-
.highest_trusted_or_verified()
271+
.highest_trusted_or_verified_before(target_height)
272+
.or_else(|| state.light_store.lowest_trusted_or_verified())
271273
.ok_or_else(Error::no_initial_trusted_state)?;
272274

273275
Err(Error::target_lower_than_trusted_state(
@@ -304,7 +306,8 @@ impl LightClient {
304306

305307
let root = state
306308
.light_store
307-
.highest_trusted_or_verified()
309+
.highest_trusted_or_verified_before(target_height)
310+
.or_else(|| state.light_store.lowest_trusted_or_verified())
308311
.ok_or_else(Error::no_initial_trusted_state)?;
309312

310313
assert!(root.height() >= target_height);

light-client/src/store.rs

+13
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ pub trait LightStore: Debug + Send + Sync {
4343
/// Get the light block of greatest height with the given status.
4444
fn highest(&self, status: Status) -> Option<LightBlock>;
4545

46+
/// Get the light block of greatest hight before the given height with the given status.
47+
fn highest_before(&self, height: Height, status: Status) -> Option<LightBlock>;
48+
4649
/// Get the light block of lowest height with the given status.
4750
fn lowest(&self, status: Status) -> Option<LightBlock>;
4851

@@ -76,6 +79,16 @@ pub trait LightStore: Debug + Send + Sync {
7679
})
7780
}
7881

82+
/// Get the first light block before the given height with the trusted or verified status.
83+
fn highest_trusted_or_verified_before(&self, height: Height) -> Option<LightBlock> {
84+
let highest_trusted = self.highest_before(height, Status::Trusted);
85+
let highest_verified = self.highest_before(height, Status::Verified);
86+
87+
std_ext::option::select(highest_trusted, highest_verified, |t, v| {
88+
std_ext::cmp::max_by_key(t, v, |lb| lb.height())
89+
})
90+
}
91+
7992
/// Get the light block of lowest height with the trusted or verified status.
8093
fn lowest_trusted_or_verified(&self) -> Option<LightBlock> {
8194
let lowest_trusted = self.lowest(Status::Trusted);

light-client/src/store/memory.rs

+9
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ impl LightStore for MemoryStore {
7272
.map(|(_, e)| e.light_block.clone())
7373
}
7474

75+
fn highest_before(&self, height: Height, status: Status) -> Option<LightBlock> {
76+
self.store
77+
.iter()
78+
.filter(|(_, e)| e.status == status)
79+
.filter(|(h, _)| h <= &&height)
80+
.max_by_key(|(&height, _)| height)
81+
.map(|(_, e)| e.light_block.clone())
82+
}
83+
7584
fn lowest(&self, status: Status) -> Option<LightBlock> {
7685
self.store
7786
.iter()

light-client/src/store/sled.rs

+22
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ impl LightStore for SledStore {
7979
self.db(status).iter().next_back()
8080
}
8181

82+
fn highest_before(&self, height: Height, status: Status) -> Option<LightBlock> {
83+
self.db(status).range(..=height).next_back()
84+
}
85+
8286
fn lowest(&self, status: Status) -> Option<LightBlock> {
8387
self.db(status).iter().next()
8488
}
@@ -109,6 +113,24 @@ mod tests {
109113
})
110114
}
111115

116+
#[test]
117+
fn highest_before_returns_correct_block() {
118+
with_blocks(10, |mut db, blocks| {
119+
for block in blocks {
120+
db.insert(block.clone(), Status::Verified);
121+
assert_eq!(
122+
db.highest_before(block.height(), Status::Verified).as_ref(),
123+
Some(&block)
124+
);
125+
assert_eq!(
126+
db.highest_before(block.height().increment(), Status::Verified)
127+
.as_ref(),
128+
Some(&block)
129+
);
130+
}
131+
})
132+
}
133+
112134
#[test]
113135
fn lowest_returns_earliest_block() {
114136
with_blocks(10, |mut db, blocks| {

light-client/src/store/sled/utils.rs

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! CBOR binary encoding.
44
55
use std::marker::PhantomData;
6+
use std::ops::{Bound, RangeBounds};
67

78
use serde::{de::DeserializeOwned, Serialize};
89

@@ -32,6 +33,15 @@ fn key_bytes(height: Height) -> [u8; 8] {
3233
height.value().to_be_bytes()
3334
}
3435

36+
// Can be removed once bound_map is stabilized. See https://github.com/rust-lang/rust/issues/86026
37+
fn map_bound(bound: Bound<&Height>) -> Bound<[u8; 8]> {
38+
match bound {
39+
Bound::Included(h) => Bound::Included(key_bytes(*h)),
40+
Bound::Excluded(h) => Bound::Excluded(key_bytes(*h)),
41+
Bound::Unbounded => Bound::Unbounded,
42+
}
43+
}
44+
3545
impl<V> HeightIndexedDb<V>
3646
where
3747
V: Serialize + DeserializeOwned,
@@ -85,6 +95,19 @@ where
8595
.flatten()
8696
.flat_map(|(_, v)| serde_cbor::from_slice(&v))
8797
}
98+
99+
/// Return an iterator over the given range
100+
pub fn range<R>(&self, range: R) -> impl DoubleEndedIterator<Item = V>
101+
where
102+
R: RangeBounds<Height>,
103+
{
104+
let range = (map_bound(range.start_bound()), map_bound(range.end_bound()));
105+
106+
self.tree
107+
.range(range)
108+
.flatten()
109+
.flat_map(|(_, v)| serde_cbor::from_slice(&v))
110+
}
88111
}
89112

90113
#[cfg(test)]

light-client/src/supervisor.rs

+62-7
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,14 @@ mod tests {
483483
let mut light_store = MemoryStore::new();
484484
light_store.insert(trusted_state, Status::Trusted);
485485

486+
for extra_trusted_height in trust_options.extra_heights {
487+
let trusted_state = io
488+
.fetch_light_block(AtHeight::At(extra_trusted_height))
489+
.expect("could not 'request' light block");
490+
491+
light_store.insert(trusted_state, Status::Trusted);
492+
}
493+
486494
let state = State {
487495
light_store: Box::new(light_store),
488496
verification_trace: HashMap::new(),
@@ -526,17 +534,12 @@ mod tests {
526534
)
527535
}
528536

529-
fn make_peer_list(
537+
fn make_peer_list_opts(
530538
primary: Option<Vec<LightBlock>>,
531539
witnesses: Option<Vec<Vec<LightBlock>>>,
532540
now: Time,
541+
trust_options: TrustOptions,
533542
) -> PeerList<Instance> {
534-
let trust_options = TrustOptions {
535-
period: DurationStr(Duration::new(604800, 0)),
536-
height: Height::try_from(1_u64).expect("Error while making height"),
537-
trust_level: TrustThresholdFraction::TWO_THIRDS,
538-
};
539-
540543
let mut peer_list = PeerList::builder();
541544

542545
if let Some(primary) = primary {
@@ -559,6 +562,24 @@ mod tests {
559562
peer_list.build()
560563
}
561564

565+
fn make_peer_list(
566+
primary: Option<Vec<LightBlock>>,
567+
witnesses: Option<Vec<Vec<LightBlock>>>,
568+
now: Time,
569+
) -> PeerList<Instance> {
570+
make_peer_list_opts(
571+
primary,
572+
witnesses,
573+
now,
574+
TrustOptions {
575+
period: DurationStr(Duration::new(604800, 0)),
576+
height: Height::try_from(1_u64).expect("Error while making height"),
577+
extra_heights: vec![],
578+
trust_level: TrustThresholdFraction::TWO_THIRDS,
579+
},
580+
)
581+
}
582+
562583
fn change_provider(
563584
mut light_blocks: Vec<LightBlock>,
564585
peer_id: Option<&str>,
@@ -859,4 +880,38 @@ mod tests {
859880
.iter()
860881
.any(|&peer| peer == primary[0].provider));
861882
}
883+
884+
#[test]
885+
fn test_bisection_between_trusted_heights() {
886+
let chain = LightChain::default_with_length(10);
887+
let primary = chain
888+
.light_blocks
889+
.into_iter()
890+
.map(|lb| lb.generate().unwrap().into_light_block())
891+
.collect::<Vec<LightBlock>>();
892+
893+
let witness = change_provider(primary.clone(), None);
894+
895+
// Specify two trusted heights (1 and 8), then attempt to verify target height 4 which
896+
// falls between the two. Verification should use regular bisection.
897+
898+
let peer_list = make_peer_list_opts(
899+
Some(primary.clone()),
900+
Some(vec![witness]),
901+
get_time(11).unwrap(),
902+
TrustOptions {
903+
period: DurationStr(Duration::new(604800, 0)),
904+
height: Height::try_from(1_u64).expect("Error while making height"),
905+
extra_heights: vec![Height::try_from(8_u64).expect("Error while making height")],
906+
trust_level: TrustThresholdFraction::TWO_THIRDS,
907+
},
908+
);
909+
910+
let (result, _) = run_bisection_test(peer_list, 4);
911+
912+
let expected_state = primary[3].clone();
913+
let new_state = result.unwrap();
914+
915+
assert_eq!(expected_state, new_state);
916+
}
862917
}

light-client/src/tests.rs

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ pub struct Provider<LB> {
7777
pub struct TrustOptions {
7878
pub period: DurationStr,
7979
pub height: HeightStr,
80+
#[serde(default)]
81+
pub extra_heights: Vec<HeightStr>,
8082
pub trust_level: TrustThreshold,
8183
}
8284

0 commit comments

Comments
 (0)