diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 315c2385d94..3f506b8a000 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -196,6 +196,21 @@ async fn rpc_getblock() { assert_eq!(get_block, expected_result); } + // Test negative heights: -1 should return block 10, -2 block 9, etc. + for neg_height in (-10..=-1).rev() { + // Convert negative height to corresponding index + let index = (neg_height + (blocks.len() as i32)) as usize; + + let expected_result = GetBlock::Raw(blocks[index].clone().into()); + + let get_block = rpc + .get_block(neg_height.to_string(), Some(0u8)) + .await + .expect("We should have a GetBlock struct"); + + assert_eq!(get_block, expected_result); + } + // Create empty note commitment tree information. let sapling = SaplingTrees { size: 0 }; let orchard = OrchardTrees { size: 0 }; diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index d5eb92e2d90..f6f34d0c223 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -84,6 +84,8 @@ pub enum HashOrHeight { Hash(block::Hash), /// A block identified by height. Height(block::Height), + /// A block identified by a negative height. + NegativeHeight(i64), } impl HashOrHeight { @@ -96,6 +98,7 @@ impl HashOrHeight { match self { HashOrHeight::Hash(hash) => op(hash), HashOrHeight::Height(height) => Some(height), + HashOrHeight::NegativeHeight(_) => None, } } @@ -113,6 +116,7 @@ impl HashOrHeight { match self { HashOrHeight::Hash(hash) => Some(hash), HashOrHeight::Height(height) => op(height), + HashOrHeight::NegativeHeight(_) => None, } } @@ -133,6 +137,16 @@ impl HashOrHeight { None } } + + /// Returns the height if this is a [`HashOrHeight::NegativeHeight`] and no overflow occurs. + pub fn height_from_negative_height(&self, tip: block::Height) -> Option { + if let HashOrHeight::NegativeHeight(index) = self { + let new_height = i64::from(tip.0).checked_add(index + 1)?; + u32::try_from(new_height).ok().map(block::Height) + } else { + None + } + } } impl std::fmt::Display for HashOrHeight { @@ -140,6 +154,7 @@ impl std::fmt::Display for HashOrHeight { match self { HashOrHeight::Hash(hash) => write!(f, "{hash}"), HashOrHeight::Height(height) => write!(f, "{}", height.0), + HashOrHeight::NegativeHeight(index) => write!(f, "{}", index), } } } @@ -176,6 +191,7 @@ impl std::str::FromStr for HashOrHeight { s.parse() .map(Self::Hash) .or_else(|_| s.parse().map(Self::Height)) + .or_else(|_| s.parse().map(|index: i64| Self::NegativeHeight(index))) .map_err(|_| { SerializationError::Parse("could not convert the input string to a hash or height") }) diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 6ad4cd93a60..15506f43b5b 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -139,7 +139,14 @@ impl ZebraDb { #[allow(clippy::unwrap_in_result)] pub fn block(&self, hash_or_height: HashOrHeight) -> Option> { // Block - let height = hash_or_height.height_or_else(|hash| self.height(hash))?; + let height = match hash_or_height { + HashOrHeight::Hash(hash) => self.height(hash), + HashOrHeight::Height(height) => Some(height), + HashOrHeight::NegativeHeight(_) => { + hash_or_height.height_from_negative_height(self.tip()?.0) + } + }?; + let header = self.block_header(height.into())?; // Transactions diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index c7d0d2877c6..26cea78bc09 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -415,8 +415,13 @@ impl Chain { /// Returns the [`ContextuallyVerifiedBlock`] with [`block::Hash`] or /// [`Height`], if it exists in this chain. pub fn block(&self, hash_or_height: HashOrHeight) -> Option<&ContextuallyVerifiedBlock> { - let height = - hash_or_height.height_or_else(|hash| self.height_by_hash.get(&hash).cloned())?; + let height = match hash_or_height { + HashOrHeight::Hash(hash) => self.height_by_hash.get(&hash).cloned(), + HashOrHeight::Height(height) => Some(height), + HashOrHeight::NegativeHeight(_) => { + hash_or_height.height_from_negative_height(self.non_finalized_tip_height()) + } + }?; self.blocks.get(&height) } @@ -507,6 +512,7 @@ impl Chain { match hash_or_height { Hash(hash) => self.contains_block_hash(hash), Height(height) => self.contains_block_height(height), + NegativeHeight(_) => false, } }