Skip to content

Commit 81050ba

Browse files
committed
ibc: prevent invalid Height from being deserialised
Change revision_height from u64 to NonZeroU64 to make invalid heights (ones where revision_height is zero) impossible to represent. This makes borsh and serde deserialisation fail when given invalid height (while in the past they would construct an invalid object).
1 parent 553bc79 commit 81050ba

File tree

2 files changed

+68
-58
lines changed

2 files changed

+68
-58
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- Prevent borsh and serde from deserialising Height with zero revision
2+
height. ([\#932](https://github.com/cosmos/ibc-rs/pull/932))
3+
<!--
4+
Add your entry's details here (in Markdown format).
5+
6+
If you don't change this message, or if this file is empty, the entry will
7+
not be created. -->

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

Lines changed: 61 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Defines the core `Height` type used throughout the library
22
3-
use core::cmp::Ordering;
4-
use core::num::ParseIntError;
3+
use core::num::{NonZeroU64, ParseIntError};
54
use core::str::FromStr;
65

76
use displaydoc::Display;
@@ -28,31 +27,29 @@ use crate::prelude::*;
2827
)]
2928
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3029
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31-
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
30+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3231
pub struct Height {
3332
/// Previously known as "epoch"
3433
revision_number: u64,
3534

3635
/// The height of a block
37-
revision_height: u64,
36+
revision_height: NonZeroU64,
3837
}
3938

4039
impl Height {
4140
pub fn new(revision_number: u64, revision_height: u64) -> Result<Self, ClientError> {
42-
if revision_height == 0 {
43-
return Err(ClientError::InvalidHeight);
44-
}
45-
46-
Ok(Self {
47-
revision_number,
48-
revision_height,
49-
})
41+
NonZeroU64::new(revision_height)
42+
.map(|revision_height| Self {
43+
revision_number,
44+
revision_height,
45+
})
46+
.ok_or(ClientError::InvalidHeight)
5047
}
5148

5249
pub fn min(revision_number: u64) -> Self {
5350
Self {
5451
revision_number,
55-
revision_height: 1,
52+
revision_height: NonZeroU64::MIN,
5653
}
5754
}
5855

@@ -61,13 +58,17 @@ impl Height {
6158
}
6259

6360
pub fn revision_height(&self) -> u64 {
64-
self.revision_height
61+
self.revision_height.get()
6562
}
6663

6764
pub fn add(&self, delta: u64) -> Height {
65+
let revision_height = self
66+
.revision_height
67+
.checked_add(delta)
68+
.expect("height should never overflow u64");
6869
Height {
6970
revision_number: self.revision_number,
70-
revision_height: self.revision_height + delta,
71+
revision_height,
7172
}
7273
}
7374

@@ -76,13 +77,15 @@ impl Height {
7677
}
7778

7879
pub fn sub(&self, delta: u64) -> Result<Height, ClientError> {
79-
if self.revision_height <= delta {
80-
return Err(ClientError::InvalidHeightResult);
81-
}
82-
80+
let revision_height = self
81+
.revision_height
82+
.get()
83+
.checked_sub(delta)
84+
.and_then(NonZeroU64::new)
85+
.ok_or(ClientError::InvalidHeightResult)?;
8386
Ok(Height {
8487
revision_number: self.revision_number,
85-
revision_height: self.revision_height - delta,
88+
revision_height: revision_height,
8689
})
8790
}
8891

@@ -91,28 +94,6 @@ impl Height {
9194
}
9295
}
9396

94-
impl PartialOrd for Height {
95-
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
96-
Some(self.cmp(other))
97-
}
98-
}
99-
100-
impl Ord for Height {
101-
fn cmp(&self, other: &Self) -> Ordering {
102-
if self.revision_number < other.revision_number {
103-
Ordering::Less
104-
} else if self.revision_number > other.revision_number {
105-
Ordering::Greater
106-
} else if self.revision_height < other.revision_height {
107-
Ordering::Less
108-
} else if self.revision_height > other.revision_height {
109-
Ordering::Greater
110-
} else {
111-
Ordering::Equal
112-
}
113-
}
114-
}
115-
11697
impl Protobuf<RawHeight> for Height {}
11798

11899
impl TryFrom<RawHeight> for Height {
@@ -127,7 +108,7 @@ impl From<Height> for RawHeight {
127108
fn from(ics_height: Height) -> Self {
128109
RawHeight {
129110
revision_number: ics_height.revision_number,
130-
revision_height: ics_height.revision_height,
111+
revision_height: ics_height.revision_height.get(),
131112
}
132113
}
133114
}
@@ -136,7 +117,7 @@ impl core::fmt::Debug for Height {
136117
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
137118
f.debug_struct("Height")
138119
.field("revision", &self.revision_number)
139-
.field("height", &self.revision_height)
120+
.field("height", &self.revision_height.get())
140121
.finish()
141122
}
142123
}
@@ -222,20 +203,9 @@ impl FromStr for Height {
222203

223204
#[test]
224205
fn test_valid_height() {
225-
assert_eq!(
226-
"1-1".parse::<Height>(),
227-
Ok(Height {
228-
revision_number: 1,
229-
revision_height: 1
230-
})
231-
);
232-
assert_eq!(
233-
"1-10".parse::<Height>(),
234-
Ok(Height {
235-
revision_number: 1,
236-
revision_height: 10
237-
})
238-
);
206+
assert_eq!("1-1".parse::<Height>(), Ok(Height::new(1, 1).unwrap()));
207+
assert_eq!("1-10".parse::<Height>(), Ok(Height::new(1, 10).unwrap()));
208+
assert_eq!("10-1".parse::<Height>(), Ok(Height::new(10, 1).unwrap()));
239209
}
240210

241211
#[test]
@@ -261,3 +231,36 @@ fn test_invalid_height() {
261231
})
262232
);
263233
}
234+
235+
#[cfg(feature = "borsh")]
236+
#[test]
237+
fn test_borsh() {
238+
use borsh::BorshDeserialize;
239+
240+
let height = Height::new(42, 24).unwrap();
241+
let encoded = borsh::to_vec(&height).unwrap();
242+
assert_eq!(
243+
&[42, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0],
244+
encoded.as_slice()
245+
);
246+
let decoded = Height::try_from_slice(&encoded).unwrap();
247+
assert_eq!(height, decoded);
248+
249+
// Test 0 revision height doesn’t deserialize.
250+
let encoded = [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
251+
Height::try_from_slice(&encoded).unwrap_err();
252+
}
253+
254+
#[cfg(feature = "serde")]
255+
#[test]
256+
fn test_serde() {
257+
let height = Height::new(42, 24).unwrap();
258+
let encoded = serde_json::to_string(&height).unwrap();
259+
assert_eq!(r#"{"revision_number":42,"revision_height":24}"#, encoded);
260+
let decoded = serde_json::from_str::<Height>(&encoded).unwrap();
261+
assert_eq!(height, decoded);
262+
263+
// Test 0 revision height doesn’t deserialize.
264+
let encoded = r#"{"revision_number":42,"revision_height":0}"#;
265+
serde_json::from_str::<Height>(&encoded).unwrap_err();
266+
}

0 commit comments

Comments
 (0)