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

Minimal implementation of backward verification for IBC relayer #709

Merged
merged 34 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8a5820b
Use custom error type instead of anomaly::BoxError
romac Jan 6, 2021
6df08e6
prototype backward verification
romac Nov 26, 2020
7935643
backward tests
romac Nov 26, 2020
d748766
re-enabled model based tests
romac Nov 26, 2020
4e12072
fix backward algorithm
romac Dec 2, 2020
66ed3c4
optimize backward verification
romac Dec 2, 2020
def731d
disable backward opti
romac Dec 2, 2020
e54abad
Fix wrong assertion
romac Dec 3, 2020
adae637
Compute last_block_id hash when generating a light chain
romac Dec 15, 2020
e580505
Add test for light chain correctness
romac Dec 15, 2020
31ba7cc
Add property-based tests for backward verification
romac Dec 16, 2020
28e21aa
Comment out `bad` test
romac Dec 16, 2020
5e37c5d
Add more tests
romac Dec 16, 2020
a3cd242
Remove println statement
romac Dec 18, 2020
0650fe5
Use prop_assert!
romac Dec 19, 2020
cf82e7d
Formatting
romac Jan 8, 2021
cb4083d
Remove hacky backward verification test
romac Jan 8, 2021
dcc2e4e
Add negative tests for backward verification
romac Jan 8, 2021
6ba571c
Feature-guard backward verification behind "backward-verif" flag
romac Jan 22, 2021
2e141c6
Rename LightClient::verify_bisection to LightClient::verify_forward
romac Jan 22, 2021
be1179a
Update changelog
romac Jan 22, 2021
6439ccc
Update doc comments
romac Jan 22, 2021
96edef3
Formatting
romac Jan 22, 2021
14ed02d
Fixup after rebase
romac Jan 22, 2021
b022d36
Add integration test for backward verification
romac Jan 26, 2021
3753a63
Remove `backward-verif` feature in favor of `unstable`
romac Jan 26, 2021
81311d9
Merge branch 'master' into romac/backward-verif
romac Jan 26, 2021
fc98f81
Cleanup + couple comments
romac Jan 26, 2021
13b0825
Check that root state for backward verif is within trusting period
romac Jan 26, 2021
b544f71
Add doc comment
romac Jan 26, 2021
0b78243
Fix mock clock time in testgen-based tests
romac Jan 26, 2021
e892095
Remove unused import
romac Jan 26, 2021
14afa8e
Revert "Use custom error type instead of anomaly::BoxError"
romac Jan 26, 2021
50d2209
Formatting
romac Jan 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ Cargo.lock

# RPC probe results
/rpc-probe/probe-results/

# Proptest regressions dumps
**/*.proptest-regressions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we supposed to keep the regressions in version control?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I would have thought not since those are evidence of a test failing and would typically be fixed and thus obsolete before the PR gets merged. But perhaps I am understanding this wrong?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of proptest's failure persistence is that it re-runs any previous known failure cases before trying to generate any more novel ones, which provides additional assurance in future that the bug has been eliminated.

I haven't used proptest extensively though and may not be using it properly yet. In playing around with it today I found that it works well when you write failing tests for the first time (it persists the random seed it used to create that failure as claimed in the docs). When you then introduce a new failing test, it doesn't update the regressions file for some reason.

But if you delete the regressions folder entirely and both tests fail, it saves the seeds for both failing tests 🤷‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I had missed that, really neat feature actually, though it seems a bit brittle indeed in some cases.

Then it makes sense to not ignore those files and leave it it up to developers to check in these regressions in or not (one may not want to check-in a regression for a broken test).

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'd say that, with property-based testing, any regressions anyone finds should be captured and committed.

Not checking it in is analogous to writing a test locally that fails on your machine (potentially showing that something's broken), but not adding it to version control, so the bug/broken code never gets found by others 😁

10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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])
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions light-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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"
1 change: 1 addition & 0 deletions light-client/src/builder/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ impl LightClientBuilder<HasTrustedState> {
self.clock,
self.scheduler,
self.verifier,
self.hasher,
self.io,
);

Expand Down
15 changes: 14 additions & 1 deletion light-client/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
152 changes: 143 additions & 9 deletions light-client/src/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,10 +53,15 @@ pub struct LightClient {
pub peer: PeerId,
/// Options for this light client
pub options: Options,

clock: Box<dyn Clock>,
scheduler: Box<dyn Scheduler>,
verifier: Box<dyn Verifier>,
io: Box<dyn Io>,

// Only used in verify_backwards when "unstable" feature is enabled
#[allow(dead_code)]
hasher: Box<dyn Hasher>,
}

impl fmt::Debug for LightClient {
Expand All @@ -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 {
Expand All @@ -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),
}
}
Expand All @@ -95,6 +102,7 @@ impl LightClient {
clock: Box<dyn Clock>,
scheduler: Box<dyn Scheduler>,
verifier: Box<dyn Verifier>,
hasher: Box<dyn Hasher>,
io: Box<dyn Io>,
) -> Self {
Self {
Expand All @@ -103,6 +111,7 @@ impl LightClient {
clock,
scheduler,
verifier,
hasher,
io,
}
}
Expand All @@ -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]
Expand Down Expand Up @@ -158,12 +169,33 @@ impl LightClient {
target_height: Height,
state: &mut State,
) -> Result<LightBlock, Error> {
// 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<LightBlock, Error> {
let mut current_height = target_height;

loop {
Expand Down Expand Up @@ -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<LightBlock, Error> {
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<LightBlock, Error> {
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(&current.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`).
///
Expand Down
6 changes: 3 additions & 3 deletions light-client/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LightBlock> {
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())
})
}
Expand Down
2 changes: 1 addition & 1 deletion light-client/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct Initial {
}

#[derive(Deserialize, Clone, Debug)]
pub struct TestBisection<LB> {
pub struct LightClientTest<LB> {
pub description: String,
pub trust_options: TrustOptions,
pub primary: Provider<LB>,
Expand Down
2 changes: 1 addition & 1 deletion light-client/src/utils/std_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub mod cmp {

/// Stable version of `std::cmp::max_by_key`.
pub fn max_by_key<A, B: Ord>(a: A, b: A, key: impl Fn(&A) -> B) -> A {
if key(&a) >= key(&b) {
if key(&a) > key(&b) {
a
} else {
b
Expand Down
Loading