Skip to content

Commit d062728

Browse files
feat: implement consensus state pruning for Tendermint clients (#922)
* Add prune_oldest_consensus_state to ClientStateValidation * Add consensus_state_heights fn signature to CommonContext * Stub prune_oldest_consensus_state fn * Add comments for delete_update_time and delete_update_height * Clean up code structure a bit * Fix some compilation errors * Address all compilation errors resulting from prune_old_consensus_states * Move delete_update_height and delete_update_time methods to ExecutionContext trait * Clean up all compilation errors * Disambiguate some method calls * Fix fn formatting * Run cargo fmt * Flip consensus state condition check * Add changelog entry * Incorporate PR feedback * test: add test_cons_state_pruning * Simplify consensus state pruning unit test a bit * Document `test_consensus_state_pruning` function * Correct a detail in documentation --------- Co-authored-by: Farhad Shabani <[email protected]>
1 parent 27592c4 commit d062728

File tree

7 files changed

+254
-13
lines changed

7 files changed

+254
-13
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Implement consensus state pruning for Tendermint light clients ([#600](https://github.com/cosmos/ibc-rs/issues/600))

crates/ibc/src/clients/ics07_tendermint/client_state.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::clients::ics07_tendermint::consensus_state::ConsensusState as TmConse
2929
use crate::clients::ics07_tendermint::error::Error;
3030
use crate::clients::ics07_tendermint::header::Header as TmHeader;
3131
use crate::clients::ics07_tendermint::misbehaviour::Misbehaviour as TmMisbehaviour;
32+
use crate::clients::ics07_tendermint::CommonContext;
3233
use crate::core::ics02_client::client_state::{
3334
ClientStateCommon, ClientStateExecution, ClientStateValidation, Status, UpdateKind,
3435
};
@@ -46,6 +47,7 @@ use crate::core::ics24_host::path::{
4647
ClientConsensusStatePath, ClientStatePath, Path, UpgradeClientPath,
4748
};
4849
use crate::core::timestamp::ZERO_DURATION;
50+
use crate::core::ExecutionContext;
4951
use crate::prelude::*;
5052
use crate::Height;
5153

@@ -488,7 +490,7 @@ where
488490

489491
impl<E> ClientStateExecution<E> for ClientState
490492
where
491-
E: TmExecutionContext,
493+
E: TmExecutionContext + ExecutionContext,
492494
<E as ClientExecutionContext>::AnyClientState: From<ClientState>,
493495
<E as ClientExecutionContext>::AnyConsensusState: From<TmConsensusState>,
494496
{
@@ -498,19 +500,18 @@ where
498500
client_id: &ClientId,
499501
consensus_state: Any,
500502
) -> Result<(), ClientError> {
503+
let host_timestamp = CommonContext::host_timestamp(ctx)?;
504+
let host_height = CommonContext::host_height(ctx)?;
505+
501506
let tm_consensus_state = TmConsensusState::try_from(consensus_state)?;
502507

503508
ctx.store_client_state(ClientStatePath::new(client_id), self.clone().into())?;
504509
ctx.store_consensus_state(
505510
ClientConsensusStatePath::new(client_id, &self.latest_height),
506511
tm_consensus_state.into(),
507512
)?;
508-
ctx.store_update_time(
509-
client_id.clone(),
510-
self.latest_height(),
511-
ctx.host_timestamp()?,
512-
)?;
513-
ctx.store_update_height(client_id.clone(), self.latest_height(), ctx.host_height()?)?;
513+
ctx.store_update_time(client_id.clone(), self.latest_height(), host_timestamp)?;
514+
ctx.store_update_height(client_id.clone(), self.latest_height(), host_height)?;
514515

515516
Ok(())
516517
}
@@ -524,10 +525,12 @@ where
524525
let header = TmHeader::try_from(header)?;
525526
let header_height = header.height();
526527

528+
self.prune_oldest_consensus_state(ctx, client_id)?;
529+
527530
let maybe_existing_consensus_state = {
528531
let path_at_header_height = ClientConsensusStatePath::new(client_id, &header_height);
529532

530-
ctx.consensus_state(&path_at_header_height).ok()
533+
CommonContext::consensus_state(ctx, &path_at_header_height).ok()
531534
};
532535

533536
if maybe_existing_consensus_state.is_some() {
@@ -536,6 +539,9 @@ where
536539
//
537540
// Do nothing.
538541
} else {
542+
let host_timestamp = CommonContext::host_timestamp(ctx)?;
543+
let host_height = CommonContext::host_height(ctx)?;
544+
539545
let new_consensus_state = TmConsensusState::from(header.clone());
540546
let new_client_state = self.clone().with_header(header)?;
541547

@@ -544,8 +550,8 @@ where
544550
new_consensus_state.into(),
545551
)?;
546552
ctx.store_client_state(ClientStatePath::new(client_id), new_client_state.into())?;
547-
ctx.store_update_time(client_id.clone(), header_height, ctx.host_timestamp()?)?;
548-
ctx.store_update_height(client_id.clone(), header_height, ctx.host_height()?)?;
553+
ctx.store_update_time(client_id.clone(), header_height, host_timestamp)?;
554+
ctx.store_update_height(client_id.clone(), header_height, host_height)?;
549555
}
550556

551557
Ok(vec![header_height])
@@ -615,14 +621,16 @@ where
615621
);
616622

617623
let latest_height = new_client_state.latest_height;
624+
let host_timestamp = CommonContext::host_timestamp(ctx)?;
625+
let host_height = CommonContext::host_height(ctx)?;
618626

619627
ctx.store_client_state(ClientStatePath::new(client_id), new_client_state.into())?;
620628
ctx.store_consensus_state(
621629
ClientConsensusStatePath::new(client_id, &latest_height),
622630
new_consensus_state.into(),
623631
)?;
624-
ctx.store_update_time(client_id.clone(), latest_height, ctx.host_timestamp()?)?;
625-
ctx.store_update_height(client_id.clone(), latest_height, ctx.host_height()?)?;
632+
ctx.store_update_time(client_id.clone(), latest_height, host_timestamp)?;
633+
ctx.store_update_height(client_id.clone(), latest_height, host_height)?;
626634

627635
Ok(latest_height)
628636
}

crates/ibc/src/clients/ics07_tendermint/client_state/update_client.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ use super::{check_header_trusted_next_validator_set, ClientState};
55
use crate::clients::ics07_tendermint::consensus_state::ConsensusState as TmConsensusState;
66
use crate::clients::ics07_tendermint::error::{Error, IntoResult};
77
use crate::clients::ics07_tendermint::header::Header as TmHeader;
8-
use crate::clients::ics07_tendermint::ValidationContext as TmValidationContext;
8+
use crate::clients::ics07_tendermint::{CommonContext, ValidationContext as TmValidationContext};
9+
use crate::core::ics02_client::consensus_state::ConsensusState;
910
use crate::core::ics02_client::error::ClientError;
11+
use crate::core::ics02_client::ClientExecutionContext;
1012
use crate::core::ics24_host::identifier::ClientId;
1113
use crate::core::ics24_host::path::ClientConsensusStatePath;
1214
use crate::prelude::*;
@@ -163,4 +165,47 @@ impl ClientState {
163165
}
164166
}
165167
}
168+
169+
pub fn prune_oldest_consensus_state<E>(
170+
&self,
171+
ctx: &mut E,
172+
client_id: &ClientId,
173+
) -> Result<(), ClientError>
174+
where
175+
E: ClientExecutionContext + CommonContext,
176+
{
177+
let mut heights = ctx.consensus_state_heights(client_id)?;
178+
179+
heights.sort();
180+
181+
for height in heights {
182+
let client_consensus_state_path = ClientConsensusStatePath::new(client_id, &height);
183+
let consensus_state =
184+
CommonContext::consensus_state(ctx, &client_consensus_state_path)?;
185+
let tm_consensus_state: TmConsensusState =
186+
consensus_state
187+
.try_into()
188+
.map_err(|err| ClientError::Other {
189+
description: err.to_string(),
190+
})?;
191+
192+
let host_timestamp = ctx.host_timestamp()?;
193+
let tm_consensus_state_expiry = (tm_consensus_state.timestamp() + self.trusting_period)
194+
.map_err(|_| ClientError::Other {
195+
description: String::from("Timestamp overflow error occurred while attempting to parse TmConsensusState")
196+
})?;
197+
198+
if tm_consensus_state_expiry > host_timestamp {
199+
break;
200+
} else {
201+
let client_id = client_id.clone();
202+
203+
ctx.delete_consensus_state(client_consensus_state_path)?;
204+
ctx.delete_update_time(client_id.clone(), height)?;
205+
ctx.delete_update_height(client_id, height)?;
206+
}
207+
}
208+
209+
Ok(())
210+
}
166211
}

crates/ibc/src/clients/ics07_tendermint/context.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::core::ics24_host::identifier::ClientId;
66
use crate::core::ics24_host::path::ClientConsensusStatePath;
77
use crate::core::timestamp::Timestamp;
88
use crate::core::ContextError;
9+
use crate::prelude::*;
910
use crate::Height;
1011

1112
/// Client's context required during both validation and execution
@@ -27,6 +28,9 @@ pub trait CommonContext {
2728
&self,
2829
client_cons_state_path: &ClientConsensusStatePath,
2930
) -> Result<Self::AnyConsensusState, ContextError>;
31+
32+
/// Returns all the heights at which a consensus state is stored
33+
fn consensus_state_heights(&self, client_id: &ClientId) -> Result<Vec<Height>, ContextError>;
3034
}
3135

3236
/// Client's context required during validation

crates/ibc/src/core/ics02_client/context.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ pub trait ClientExecutionContext: Sized {
5353
consensus_state: Self::AnyConsensusState,
5454
) -> Result<(), ContextError>;
5555

56+
/// Delete the consensus state from the store located at the given `ClientConsensusStatePath`
57+
fn delete_consensus_state(
58+
&mut self,
59+
consensus_state_path: ClientConsensusStatePath,
60+
) -> Result<(), ContextError>;
61+
5662
/// Called upon successful client update.
5763
/// Implementations are expected to use this to record the specified time as the time at which
5864
/// this update (or header) was processed.
@@ -72,4 +78,22 @@ pub trait ClientExecutionContext: Sized {
7278
height: Height,
7379
host_height: Height,
7480
) -> Result<(), ContextError>;
81+
82+
/// Delete the update time associated with the client at the specified height. This update
83+
/// time should be associated with a consensus state through the specified height.
84+
///
85+
/// Note that this timestamp is determined by the host.
86+
fn delete_update_time(
87+
&mut self,
88+
client_id: ClientId,
89+
height: Height,
90+
) -> Result<(), ContextError>;
91+
92+
/// Delete the update height associated with the client at the specified height. This update
93+
/// time should be associated with a consensus state through the specified height.
94+
fn delete_update_height(
95+
&mut self,
96+
client_id: ClientId,
97+
height: Height,
98+
) -> Result<(), ContextError>;
7599
}

crates/ibc/src/core/ics02_client/handler/update_client.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,10 @@ mod tests {
139139
use crate::core::ics02_client::handler::update_client::{execute, validate};
140140
use crate::core::ics02_client::msgs::misbehaviour::MsgSubmitMisbehaviour;
141141
use crate::core::ics02_client::msgs::update_client::MsgUpdateClient;
142+
use crate::core::ics02_client::ClientValidationContext;
142143
use crate::core::ics23_commitment::specs::ProofSpecs;
143144
use crate::core::ics24_host::identifier::{ChainId, ClientId};
145+
use crate::core::ics24_host::path::ClientConsensusStatePath;
144146
use crate::core::timestamp::Timestamp;
145147
use crate::mock::client_state::{client_type as mock_client_type, MockClientState};
146148
use crate::mock::context::{
@@ -180,6 +182,96 @@ mod tests {
180182
);
181183
}
182184

185+
/// Tests that the Tendermint client consensus state pruning logic
186+
/// functions correctly.
187+
///
188+
/// This test sets up a MockContext with host height 1 and a trusting
189+
/// period of 3 seconds. It then advances the state of the MockContext
190+
/// by 2 heights, and thus 6 seconds, due to the DEFAULT_BLOCK_TIME_SECS
191+
/// constant being set to 3 seconds. At this point, the chain is at height
192+
/// 3. Any consensus states associated with a block more than 3 seconds
193+
/// in the past should be expired and pruned from the IBC store. The test
194+
/// thus checks that the consensus state at height 1 is not contained in
195+
/// the store. It also checks that the consensus state at height 2 is
196+
/// contained in the store and has not expired.
197+
#[test]
198+
fn test_consensus_state_pruning() {
199+
let chain_id = ChainId::new("mockgaiaA", 1).unwrap();
200+
201+
let client_height = Height::new(1, 1).unwrap();
202+
203+
let client_id = ClientId::new(tm_client_type(), 0).unwrap();
204+
205+
let mut ctx = MockContextConfig::builder()
206+
.host_id(chain_id.clone())
207+
.host_type(HostType::SyntheticTendermint)
208+
.latest_height(client_height)
209+
.latest_timestamp(Timestamp::now())
210+
.max_history_size(u64::MAX)
211+
.build()
212+
.with_client_config(
213+
MockClientConfig::builder()
214+
.client_chain_id(chain_id.clone())
215+
.client_id(client_id.clone())
216+
.client_state_height(client_height)
217+
.client_type(tm_client_type())
218+
.trusting_period(Duration::from_secs(3))
219+
.build(),
220+
);
221+
222+
let start_host_timestamp = ctx.host_timestamp().unwrap();
223+
224+
// Move the chain forward by 2 blocks to pass the trusting period.
225+
for _ in 1..=2 {
226+
let signer = get_dummy_account_id();
227+
228+
let update_height = ctx.latest_height();
229+
230+
ctx.advance_host_chain_height();
231+
232+
let mut block = ctx.host_block(&update_height).unwrap().clone();
233+
234+
block.set_trusted_height(client_height);
235+
236+
let msg = MsgUpdateClient {
237+
client_id: client_id.clone(),
238+
client_message: block.clone().into(),
239+
signer,
240+
};
241+
242+
let _ = validate(&ctx, MsgUpdateOrMisbehaviour::UpdateClient(msg.clone()));
243+
let _ = execute(&mut ctx, MsgUpdateOrMisbehaviour::UpdateClient(msg.clone()));
244+
}
245+
246+
// Check that latest expired consensus state is pruned.
247+
let expired_height = Height::new(1, 1).unwrap();
248+
let client_cons_state_path = ClientConsensusStatePath::new(&client_id, &expired_height);
249+
assert!(ctx
250+
.client_update_height(&client_id, &expired_height)
251+
.is_err());
252+
assert!(ctx.client_update_time(&client_id, &expired_height).is_err());
253+
assert!(ctx.consensus_state(&client_cons_state_path).is_err());
254+
255+
// Check that latest valid consensus state exists.
256+
let earliest_valid_height = Height::new(1, 2).unwrap();
257+
let client_cons_state_path =
258+
ClientConsensusStatePath::new(&client_id, &earliest_valid_height);
259+
260+
assert!(ctx
261+
.client_update_height(&client_id, &earliest_valid_height)
262+
.is_ok());
263+
assert!(ctx
264+
.client_update_time(&client_id, &earliest_valid_height)
265+
.is_ok());
266+
assert!(ctx.consensus_state(&client_cons_state_path).is_ok());
267+
268+
let end_host_timestamp = ctx.host_timestamp().unwrap();
269+
assert_eq!(
270+
end_host_timestamp,
271+
(start_host_timestamp + Duration::from_secs(6)).unwrap()
272+
);
273+
}
274+
183275
#[test]
184276
fn test_update_nonexisting_client() {
185277
let client_id = ClientId::from_str("mockclient1").unwrap();

0 commit comments

Comments
 (0)