Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve sled-based light store #773

Merged
merged 13 commits into from
Jan 22, 2021
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@

### BUG FIXES

* `[light-client]` Fix potential block ordering problem with sled-based lightstore ([#769])
* `[light-client]` Improve the API of the light store. ([#428])
* `[light-client]` The `sled`-backed lightstore is now feature-guarded under
the `lightstore-sled` feature, which is enabled by default for now. ([#772])
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
[#772]: https://github.com/informalsystems/tendermint-rs/pull/772
[#428]: https://github.com/informalsystems/tendermint-rs/issues/428

## v0.17.1

Expand All @@ -36,8 +39,8 @@ Client's correct and reliable operation.

### BUG FIXES:

- `[tendermint]` `Time` values were not always formatted properly,
causing the light client to sometimes return malformed light blocks. ([#774])
* `[tendermint]` `Time` values were not always formatted properly,
causing the light client to sometimes return malformed light blocks ([#774])

[#774]: https://github.com/informalsystems/tendermint-rs/issues/774

Expand Down
1 change: 1 addition & 0 deletions light-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ tendermint-testgen = { path = "../testgen"}
serde_json = "1.0.51"
gumdrop = "0.8.0"
rand = "0.7.3"
tempdir = "0.3.7"
2 changes: 1 addition & 1 deletion light-client/src/builder/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl LightClientBuilder<NoTrustedState> {
pub fn trust_from_store(self) -> Result<LightClientBuilder<HasTrustedState>, Error> {
let trusted_state = self
.light_store
.latest_trusted_or_verified()
.highest_trusted_or_verified()
.ok_or(error::Kind::NoTrustedStateInStore)?;

self.trust_light_block(trusted_state)
Expand Down
8 changes: 4 additions & 4 deletions light-client/src/components/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub trait Scheduler: Send + Sync {
///
/// ## Postcondition
/// - The resulting height must be valid according to `valid_schedule`. [LCV-SCHEDULE-POST.1]
#[pre(light_store.latest_trusted_or_verified().is_some())]
#[pre(light_store.highest_trusted_or_verified().is_some())]
#[post(valid_schedule(ret, target_height, current_height, light_store))]
fn schedule(
&self,
Expand Down Expand Up @@ -53,15 +53,15 @@ where
///
/// ## Postcondition
/// - The resulting height must be valid according to `valid_schedule`. [LCV-SCHEDULE-POST.1]
#[pre(light_store.latest_trusted_or_verified().is_some())]
#[pre(light_store.highest_trusted_or_verified().is_some())]
#[post(valid_schedule(ret, target_height, current_height, light_store))]
pub fn basic_bisecting_schedule(
light_store: &dyn LightStore,
current_height: Height,
target_height: Height,
) -> Height {
let trusted_height = light_store
.latest_trusted_or_verified()
.highest_trusted_or_verified()
.map(|lb| lb.height())
.unwrap();

Expand Down Expand Up @@ -105,7 +105,7 @@ pub fn valid_schedule(
light_store: &dyn LightStore,
) -> bool {
let latest_trusted_height = light_store
.latest_trusted_or_verified()
.highest_trusted_or_verified()
.map(|lb| lb.height())
.unwrap();

Expand Down
2 changes: 1 addition & 1 deletion light-client/src/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ impl LightClient {
// Get the latest trusted state
let trusted_state = state
.light_store
.latest_trusted_or_verified()
.highest_trusted_or_verified()
.ok_or(ErrorKind::NoInitialTrustedState)?;

if target_height < trusted_state.height() {
Expand Down
21 changes: 17 additions & 4 deletions light-client/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ pub trait LightStore: Debug + Send + Sync {
fn remove(&mut self, height: Height, status: Status);

/// Get the light block of greatest height with the given status.
fn latest(&self, status: Status) -> Option<LightBlock>;
fn highest(&self, status: Status) -> Option<LightBlock>;

/// Get the light block of lowest height with the given status.
fn lowest(&self, status: Status) -> Option<LightBlock>;

/// Get an iterator of all light blocks with the given status.
fn all(&self, status: Status) -> Box<dyn Iterator<Item = LightBlock>>;
Expand All @@ -62,15 +65,25 @@ pub trait LightStore: Debug + Send + Sync {
}

/// Get the light block of greatest height with the trusted or verified status.
fn latest_trusted_or_verified(&self) -> Option<LightBlock> {
let latest_trusted = self.latest(Status::Trusted);
let latest_verified = self.latest(Status::Verified);
fn highest_trusted_or_verified(&self) -> Option<LightBlock> {
let latest_trusted = self.highest(Status::Trusted);
let latest_verified = self.highest(Status::Verified);

std_ext::option::select(latest_trusted, latest_verified, |t, v| {
std_ext::cmp::max_by_key(t, v, |lb| lb.height())
})
}

/// Get the light block of lowest height with the trusted or verified status.
fn lowest_trusted_or_verified(&self) -> Option<LightBlock> {
let latest_trusted = self.lowest(Status::Trusted);
let latest_verified = self.lowest(Status::Verified);

std_ext::option::select(latest_trusted, latest_verified, |t, v| {
std_ext::cmp::min_by_key(t, v, |lb| lb.height())
})
}

/// Get the light block of the given height with the trusted or verified status.
fn get_trusted_or_verified(&self, height: Height) -> Option<LightBlock> {
self.get(height, Status::Trusted)
Expand Down
10 changes: 9 additions & 1 deletion light-client/src/store/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,22 @@ impl LightStore for MemoryStore {
self.insert(light_block.clone(), status);
}

fn latest(&self, status: Status) -> Option<LightBlock> {
fn highest(&self, status: Status) -> Option<LightBlock> {
self.store
.iter()
.filter(|(_, e)| e.status == status)
.max_by_key(|(&height, _)| height)
.map(|(_, e)| e.light_block.clone())
}

fn lowest(&self, status: Status) -> Option<LightBlock> {
self.store
.iter()
.filter(|(_, e)| e.status == status)
.min_by_key(|(&height, _)| height)
.map(|(_, e)| e.light_block.clone())
}

fn all(&self, status: Status) -> Box<dyn Iterator<Item = LightBlock>> {
let light_blocks: Vec<_> = self
.store
Expand Down
110 changes: 83 additions & 27 deletions light-client/src/store/sled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,25 @@ pub mod utils;
use std::path::Path;

use crate::{
store::sled::utils::*,
store::sled::utils::HeightIndexedDb,
types::{Height, LightBlock},
};

use super::{LightStore, Status};
use ::sled::Db as SledDb;

const UNVERIFIED_PREFIX: &str = "light_store/unverified";
const VERIFIED_PREFIX: &str = "light_store/verified";
const TRUSTED_PREFIX: &str = "light_store/trusted";
const FAILED_PREFIX: &str = "light_store/failed";
const UNVERIFIED: &str = "unverified";
const VERIFIED: &str = "verified";
const TRUSTED: &str = "trusted";
const FAILED: &str = "failed";

/// Persistent store backed by an on-disk `sled` database.
#[derive(Debug, Clone)]
pub struct SledStore {
db: SledDb,
unverified_db: KeyValueDb<Height, LightBlock>,
verified_db: KeyValueDb<Height, LightBlock>,
trusted_db: KeyValueDb<Height, LightBlock>,
failed_db: KeyValueDb<Height, LightBlock>,
unverified_db: HeightIndexedDb<LightBlock>,
verified_db: HeightIndexedDb<LightBlock>,
trusted_db: HeightIndexedDb<LightBlock>,
failed_db: HeightIndexedDb<LightBlock>,
db: sled::Db,
}

impl SledStore {
Expand All @@ -34,17 +33,17 @@ impl SledStore {
}

/// Create a new persistent store from a sled database that is already open.
pub fn new(db: SledDb) -> Self {
pub fn new(db: sled::Db) -> Self {
Self {
unverified_db: HeightIndexedDb::new(db.open_tree(UNVERIFIED).unwrap()),
verified_db: HeightIndexedDb::new(db.open_tree(VERIFIED).unwrap()),
trusted_db: HeightIndexedDb::new(db.open_tree(TRUSTED).unwrap()),
failed_db: HeightIndexedDb::new(db.open_tree(FAILED).unwrap()),
db,
unverified_db: KeyValueDb::new(UNVERIFIED_PREFIX),
verified_db: KeyValueDb::new(VERIFIED_PREFIX),
trusted_db: KeyValueDb::new(TRUSTED_PREFIX),
failed_db: KeyValueDb::new(FAILED_PREFIX),
}
}

fn db(&self, status: Status) -> &KeyValueDb<Height, LightBlock> {
fn db(&self, status: Status) -> &HeightIndexedDb<LightBlock> {
match status {
Status::Unverified => &self.unverified_db,
Status::Verified => &self.verified_db,
Expand All @@ -56,38 +55,95 @@ impl SledStore {

impl LightStore for SledStore {
fn get(&self, height: Height, status: Status) -> Option<LightBlock> {
self.db(status).get(&self.db, &height).ok().flatten()
self.db(status).get(height).ok().flatten()
}

fn update(&mut self, light_block: &LightBlock, status: Status) {
let height = light_block.height();

for other in Status::iter() {
if status != *other {
self.db(*other).remove(&self.db, &height).ok();
self.db(*other).remove(height).ok();
}
}

self.db(status).insert(&self.db, &height, light_block).ok();
self.db(status).insert(height, light_block).ok();
}

fn insert(&mut self, light_block: LightBlock, status: Status) {
self.db(status)
.insert(&self.db, &light_block.height(), &light_block)
.insert(light_block.height(), &light_block)
.ok();
}

fn remove(&mut self, height: Height, status: Status) {
self.db(status).remove(&self.db, &height).ok();
self.db(status).remove(height).ok();
}

fn latest(&self, status: Status) -> Option<LightBlock> {
self.db(status)
.iter(&self.db)
.max_by(|first, second| first.height().cmp(&second.height()))
fn highest(&self, status: Status) -> Option<LightBlock> {
self.db(status).iter().next_back()
}

fn lowest(&self, status: Status) -> Option<LightBlock> {
self.db(status).iter().next()
}

fn all(&self, status: Status) -> Box<dyn Iterator<Item = LightBlock>> {
Box::new(self.db(status).iter(&self.db))
Box::new(self.db(status).iter())
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use tendermint_testgen::{light_block::TMLightBlock as TGLightBlock, Generator, LightChain};

#[test]
fn highest_returns_latest_block() {
with_blocks(10, |mut db, blocks| {
let initial_block = blocks[0].clone();
db.insert(initial_block.clone(), Status::Verified);
assert_eq!(db.lowest(Status::Verified).as_ref(), Some(&initial_block));

for block in blocks.into_iter().skip(1) {
db.insert(block, Status::Verified);
assert_eq!(db.lowest(Status::Verified).as_ref(), Some(&initial_block));
}
})
}

#[test]
fn lowest_returns_earliest_block() {
with_blocks(10, |mut db, blocks| {
for block in blocks {
db.insert(block.clone(), Status::Verified);
assert_eq!(db.highest(Status::Verified), Some(block));
}
})
}

fn with_blocks(height: u64, f: impl FnOnce(SledStore, Vec<LightBlock>)) {
let tmp_dir = TempDir::new("tendermint_light_client_sled_test").unwrap();
let db = SledStore::open(tmp_dir).unwrap();

let chain = LightChain::default_with_length(height);
let blocks = chain
.light_blocks
.into_iter()
.map(|lb| lb.generate().unwrap())
.map(testgen_to_lb)
.collect::<Vec<_>>();

f(db, blocks)
}

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,
}
}
}
Loading