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

test: tendermint client update when client expires or validator set has changed #921

Merged
merged 26 commits into from
Oct 18, 2023
Merged
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1d78333
add client expiry test
rnbguy Oct 13, 2023
90b006d
tm hostblock supports trusted next validator set
rnbguy Oct 13, 2023
6039366
fix validator change update test
rnbguy Oct 13, 2023
a2163f1
fix incorrect validator updates at each block
rnbguy Oct 13, 2023
7dfc285
add sad client update for validator change
rnbguy Oct 13, 2023
78f5eda
cargo fmt
rnbguy Oct 13, 2023
89ffdbf
catch up with main branch changes
rnbguy Oct 16, 2023
2d90996
update MockContextConfig with validator set history
rnbguy Oct 16, 2023
ef774cc
refactor tests with updated MockContextConfig
rnbguy Oct 16, 2023
2750e90
rm duplicate def of `on`
rnbguy Oct 16, 2023
25b9b4d
add todo for max_history_size and validator_set_history
rnbguy Oct 16, 2023
f05537a
consistent variable naming in tests
rnbguy Oct 17, 2023
e827067
bump typed-builder version
rnbguy Oct 17, 2023
bf55a97
rm redundant builder arguments
rnbguy Oct 17, 2023
87196d3
replace todo with panic
rnbguy Oct 17, 2023
122a570
mv Tendermint ClientStateConfig under ics07
rnbguy Oct 17, 2023
7f7f79f
use ctx_a with ctx_b instead of only ctx
rnbguy Oct 17, 2023
eb8798c
use client_id consistently
rnbguy Oct 17, 2023
6b47dc7
use mocks feature directly in dev-deps
rnbguy Oct 17, 2023
f7cc801
include trusting_period and max_clock_drift in mock light client config
rnbguy Oct 18, 2023
0d2d642
revert advance chain height with timestamp
rnbguy Oct 18, 2023
3020a41
update client expiry test
rnbguy Oct 18, 2023
1d3bba8
add test to check max_clock_drift
rnbguy Oct 18, 2023
554ac48
rm TODO comments in favor of gh issue
rnbguy Oct 18, 2023
0da962b
revert ctx_a renaming
rnbguy Oct 18, 2023
57180f9
add changelog entry
rnbguy Oct 18, 2023
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
Next Next commit
add client expiry test
rnbguy committed Oct 16, 2023
commit 1d78333d0c12079946eee46ecc2fc4879a2f95d4
4 changes: 3 additions & 1 deletion crates/ibc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ schema = ["dep:schemars", "serde", "std"]

# This feature grants access to development-time mocking libraries, such as `MockContext` or `MockHeader`.
# Depends on the `testgen` suite for generating Tendermint light blocks.
mocks = ["tendermint-testgen", "tendermint/clock", "parking_lot"]
mocks = ["tendermint-testgen", "tendermint/clock", "parking_lot", "typed-builder"]

[dependencies]
# Proto definitions for all IBC-related interfaces, e.g., connections or channels.
@@ -73,6 +73,7 @@ scale-info = { version = "2.1.2", default-features = false, features = ["derive"
## for borsh encode or decode
borsh = {version = "0.10", default-features = false, optional = true }
parking_lot = { version = "0.12.1", default-features = false, optional = true }
typed-builder = { version = "0.16.2", optional = true }

ibc-derive = { version ="0.3.0", path = "../ibc-derive" }

@@ -104,3 +105,4 @@ test-log = { version = "0.2.10", features = ["trace"] }
tendermint-rpc = { version = "0.34", features = ["http-client", "websocket-client"] }
tendermint-testgen = { version = "0.34" } # Needed for generating (synthetic) light blocks.
parking_lot = { version = "0.12.1" }
typed-builder = { version = "0.16.2" }
67 changes: 66 additions & 1 deletion crates/ibc/src/core/ics02_client/handler/update_client.rs
Original file line number Diff line number Diff line change
@@ -121,6 +121,7 @@ where

#[cfg(test)]
mod tests {
use core::ops::Add;
use core::str::FromStr;
use core::time::Duration;

@@ -143,7 +144,9 @@ mod tests {
use crate::core::ics24_host::identifier::{ChainId, ClientId};
use crate::core::timestamp::Timestamp;
use crate::mock::client_state::{client_type as mock_client_type, MockClientState};
use crate::mock::context::{AnyConsensusState, MockContext};
use crate::mock::context::{
AnyClientState, AnyConsensusState, MockClientConfig, MockContext, MockContextConfig,
};
use crate::mock::header::MockHeader;
use crate::mock::host::{HostBlock, HostType};
use crate::mock::misbehaviour::Misbehaviour as MockMisbehaviour;
@@ -753,4 +756,66 @@ mod tests {
assert!(res.is_ok());
ensure_misbehaviour(&ctx_a, &client_id, &tm_client_type());
}

#[test]
fn test_expired_client() {
let remote_chain_id = ChainId::new("mockgaiaB", 1).unwrap();
let host_chain_id = ChainId::new("mockgaiaA", 1).unwrap();

let remote_height = Height::new(1, 21).unwrap();
let remote_client_height_on_host = remote_height.sub(3).unwrap();

let remote_client_id_on_host = ClientId::new(tm_client_type(), 0).unwrap();

let timestamp = Timestamp::now();

let mut host_ctx = MockContextConfig::builder()
.host_id(host_chain_id.clone())
.latest_height(Height::new(1, 1).unwrap())
.latest_timestamp(timestamp)
.build()
.with_client_config(
MockClientConfig::builder()
.client_chain_id(remote_chain_id.clone())
.client_id(remote_client_id_on_host.clone())
.client_state_height(remote_client_height_on_host)
.client_type(tm_client_type())
.latest_timestamp(timestamp)
.build(),
);

let mut remote_ctx = MockContextConfig::builder()
.host_id(remote_chain_id.clone())
.host_type(HostType::SyntheticTendermint)
.latest_height(remote_height)
.latest_timestamp(timestamp)
.build();

{
let remote_raw_client_state = host_ctx.client_state(&remote_client_id_on_host).unwrap();

let remote_client_state = match remote_raw_client_state {
AnyClientState::Tendermint(tm_client_state) => tm_client_state,
_ => panic!("never fails. not mock client"),
};

let client_trusting_period = remote_client_state.trusting_period;

let future_timestamp = host_ctx
.host_timestamp()
.expect("never fails")
.add(client_trusting_period)
.expect("overflow");

host_ctx.advance_host_chain_height_with_timestamp(future_timestamp);
remote_ctx.advance_host_chain_height_with_timestamp(future_timestamp);
}

let remote_client_state = host_ctx.client_state(&remote_client_id_on_host).unwrap();

assert!(remote_client_state
.status(&host_ctx, &remote_client_id_on_host)
.unwrap()
.is_expired());
}
}
182 changes: 178 additions & 4 deletions crates/ibc/src/mock/context.rs
Original file line number Diff line number Diff line change
@@ -16,6 +16,8 @@ use parking_lot::Mutex;
use tendermint_testgen::Validator as TestgenValidator;
use tracing::debug;

use self::clients::TmClientStateConfig;

use super::client_state::{MOCK_CLIENT_STATE_TYPE_URL, MOCK_CLIENT_TYPE};
use super::consensus_state::MOCK_CONSENSUS_STATE_TYPE_URL;
use crate::clients::ics07_tendermint::client_state::{
@@ -54,6 +56,7 @@ use crate::mock::ics18_relayer::error::RelayerError;
use crate::prelude::*;
use crate::signer::Signer;
use crate::Height;
use typed_builder::TypedBuilder;

pub const DEFAULT_BLOCK_TIME_SECS: u64 = 3;

@@ -217,6 +220,100 @@ pub struct MockContext {
pub logs: Vec<String>,
}

#[derive(Debug, TypedBuilder)]
#[builder(build_method(into = MockContext))]
pub struct MockContextConfig {
#[builder(default = HostType::Mock)]
host_type: HostType,

host_id: ChainId,

#[builder(default = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS))]
block_time: Duration,

#[builder(default = 5)]
max_history_size: usize,

latest_height: Height,

#[builder(default = Timestamp::now())]
latest_timestamp: Timestamp,
}

impl From<MockContextConfig> for MockContext {
fn from(params: MockContextConfig) -> Self {
assert_ne!(
params.max_history_size, 0,
"The chain must have a non-zero max_history_size"
);

assert_ne!(
params.latest_height.revision_height(),
0,
"The chain must have a non-zero revision_height"
);

// Compute the number of blocks to store.
let n = min(
params.max_history_size as u64,
params.latest_height.revision_height(),
);

assert_eq!(
params.host_id.revision_number(),
params.latest_height.revision_number(),
"The version in the chain identifier must match the version in the latest height"
);

let next_block_timestamp = params
.latest_timestamp
.add(params.block_time)
.expect("Never fails");

MockContext {
host_chain_type: params.host_type,
host_chain_id: params.host_id.clone(),
max_history_size: params.max_history_size,
history: (0..n)
.rev()
.map(|i| {
// generate blocks with timestamps -> N, N - BT, N - 2BT, ...
// where N = now(), BT = block_time
HostBlock::generate_block(
params.host_id.clone(),
params.host_type,
params
.latest_height
.sub(i)
.expect("Never fails")
.revision_height(),
next_block_timestamp
.sub(params.block_time * ((i + 1) as u32))
.expect("Never fails"),
)
})
.collect(),
block_time: params.block_time,
ibc_store: Arc::new(Mutex::new(MockIbcStore::default())),
events: Vec::new(),
logs: Vec::new(),
}
}
}

#[derive(Debug, TypedBuilder)]
pub struct MockClientConfig {
client_chain_id: ChainId,
client_id: ClientId,
#[builder(default = mock_client_type())]
client_type: ClientType,
client_state_height: Height,
#[builder(default)]
consensus_state_heights: Vec<Height>,
#[builder(default = Timestamp::now())]
latest_timestamp: Timestamp,
}

/// Returns a MockContext with bare minimum initialization: no clients, no connections and no channels are
/// present, and the chain has Height(5). This should be used sparingly, mostly for testing the
/// creation of new domain objects.
@@ -554,6 +651,81 @@ impl MockContext {
self
}

pub fn with_client_config(self, client: MockClientConfig) -> Self {
let cs_heights = if client.consensus_state_heights.is_empty() {
vec![client.client_state_height]
} else {
client.consensus_state_heights
};

let (client_state, consensus_states) = match client.client_type.as_str() {
MOCK_CLIENT_TYPE => {
let blocks: Vec<_> = cs_heights
.into_iter()
.map(|cs_height| (cs_height, MockHeader::new(cs_height)))
.collect();

let client_state = MockClientState::new(blocks.last().expect("never fails").1);

let cs_states = blocks
.into_iter()
.map(|(height, block)| (height, MockConsensusState::new(block).into()))
.collect();

(client_state.into(), cs_states)
}
TENDERMINT_CLIENT_TYPE => {
let n_blocks = cs_heights.len();
let blocks: Vec<_> = cs_heights
.into_iter()
.enumerate()
.map(|(i, cs_height)| {
(
cs_height,
HostBlock::generate_tm_block(
client.client_chain_id.clone(),
cs_height.revision_height(),
client
.latest_timestamp
.sub(self.block_time * ((n_blocks - 1 - i) as u32))
.expect("never fails"),
),
)
})
.collect();

// TODO(rano): parametrize the other params
let client_state: TmClientState = TmClientStateConfig::builder()
.chain_id(client.client_chain_id)
.latest_height(client.client_state_height)
.build()
.try_into()
.expect("never fails");

client_state.validate().expect("never fails");

let cs_states = blocks
.into_iter()
.map(|(height, block)| (height, block.into()))
.collect();

(client_state.into(), cs_states)
}
_ => todo!(),
Copy link
Member

Choose a reason for hiding this comment

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

Should panic here instead of todo!() ?

};

let client_record = MockClientRecord {
client_state: Some(client_state),
consensus_states,
};

self.ibc_store
.lock()
.clients
.insert(client.client_id.clone(), client_record);
self
}

/// Associates a connection to this context.
pub fn with_connection(
self,
@@ -683,15 +855,17 @@ impl MockContext {

/// Triggers the advancing of the host chain, by extending the history of blocks (or headers).
pub fn advance_host_chain_height(&mut self) {
self.advance_host_chain_height_with_timestamp(self.host_timestamp().expect("Never fails"))
}

/// Triggers the advancing of the host chain, by extending the history of blocks (or headers).
pub fn advance_host_chain_height_with_timestamp(&mut self, timestamp: Timestamp) {
let latest_block = self.history.last().expect("history cannot be empty");
let new_block = HostBlock::generate_block(
self.host_chain_id.clone(),
self.host_chain_type,
latest_block.height().increment().revision_height(),
latest_block
.timestamp()
.add(self.block_time)
.expect("Never fails"),
timestamp,
);

// Append the new header at the tip of the history.
46 changes: 45 additions & 1 deletion crates/ibc/src/mock/context/clients.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,63 @@
//! Client context implementations for `MockContext`

use core::time::Duration;

use super::{AnyClientState, AnyConsensusState, MockClientRecord, MockContext};
use crate::clients::ics07_tendermint::client_state::AllowUpdate;
use crate::clients::ics07_tendermint::trust_threshold::TrustThreshold;
use crate::clients::ics07_tendermint::{
client_state::ClientState as TmClientState, error::Error as TmClientError,
CommonContext as TmCommonContext, ValidationContext as TmValidationContext,
};
use crate::core::ics02_client::error::ClientError;
use crate::core::ics02_client::{ClientExecutionContext, ClientValidationContext};
use crate::core::ics24_host::identifier::ClientId;
use crate::core::ics23_commitment::specs::ProofSpecs;
use crate::core::ics24_host::identifier::{ChainId, ClientId};
use crate::core::ics24_host::path::{ClientConsensusStatePath, ClientStatePath};
use crate::core::timestamp::Timestamp;
use crate::core::{ContextError, ValidationContext};
use crate::mock::client_state::MockClientContext;
use crate::prelude::*;
use crate::Height;

#[derive(typed_builder::TypedBuilder, Debug)]
pub struct TmClientStateConfig {
pub chain_id: ChainId,
#[builder(default)]
pub trust_level: TrustThreshold,
#[builder(default = Duration::from_secs(64000))]
pub trusting_period: Duration,
#[builder(default = Duration::from_secs(128000))]
pub unbonding_period: Duration,
#[builder(default = Duration::from_millis(3000))]
max_clock_drift: Duration,
pub latest_height: Height,
#[builder(default)]
pub proof_specs: ProofSpecs,
#[builder(default)]
pub upgrade_path: Vec<String>,
#[builder(default = AllowUpdate { after_expiry: false, after_misbehaviour: false })]
allow_update: AllowUpdate,
}

impl TryFrom<TmClientStateConfig> for TmClientState {
type Error = TmClientError;

fn try_from(config: TmClientStateConfig) -> Result<Self, Self::Error> {
TmClientState::new(
config.chain_id,
config.trust_level,
config.trusting_period,
config.unbonding_period,
config.max_clock_drift,
config.latest_height,
config.proof_specs,
config.upgrade_path,
config.allow_update,
)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Here, we should host the code related to the mock and implementing contexts on it.
TmClientStateConfig should be placed under the ics07_tendermint and enabled using the mocks feature flag (along with others (https://github.com/cosmos/ibc-rs/blob/245d2fc3d53d7b3c9eb6d2380580c798aa150fe9/crates/ibc/src/clients/ics07_tendermint/client_state.rs#L1122-L1183))

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes. You are right 👍


impl MockClientContext for MockContext {
type ConversionError = &'static str;
type AnyConsensusState = AnyConsensusState;