Repository: ZcashFoundation/zebra
Pull request #9408: feat(getaddresstxids): Make start and end fields optional and apply range rules to match zcashd
Author: login: oxarbitrage
Labels: name: A-rpc
description: Area: Remote Procedure Call interfaces
name: C-feature
description: Category: New features
name: A-compatibility
description: Area: Compatibility with other nodes or wallets, or standard rules
name: P-Medium :zap:
Created: 2025-04-10T00:18:38Z | Merged: 2025-04-11T14:33:55Z

## Motivation

We want to match zcashd in regards to the `start` and `end` arguments that were added in https://github.com/zcash/zcash/pull/6932

Close https://github.com/ZcashFoundation/zebra/issues/8790

This can be a breaking change for some consumers but we can keep `GetAddressTxIdsRequest::from_parts` and `GetAddressTxIdsRequest::into_parts` the same which is i think what Zaino is using.

## Solution

Make the start and end parameters optional, apply new rules.

### Tests

Existing tests were updated to cover new rules.

### PR Checklist

<!-- Check as many boxes as possible. -->

- [x] The PR name is suitable for the release notes.
- [x] The solution is tested.
- [x] The documentation is up to date.
- [x] The PR has a priority label.
- [ ] If the PR shouldn't be in the release notes, it has the
      `C-exclude-from-changelog` label.



Diff:
diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs
index fcc821204f3..fb3f596ab65 100644
--- a/zebra-rpc/src/methods.rs
+++ b/zebra-rpc/src/methods.rs
@@ -6,8 +6,11 @@
 //! Some parts of the `zcashd` RPC documentation are outdated.
 //! So this implementation follows the `zcashd` server and `lightwalletd` client implementations.
 
-use std::collections::HashMap;
-use std::{collections::HashSet, fmt::Debug};
+use std::{
+    collections::{HashMap, HashSet},
+    fmt::Debug,
+    ops::RangeInclusive,
+};
 
 use chrono::Utc;
 use futures::{stream::FuturesOrdered, StreamExt, TryFutureExt};
@@ -308,8 +311,8 @@ pub trait Rpc {
     ///
     /// - `request`: (object, required, example={\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"], \"start\": 1000, \"end\": 2000}) A struct with the following named fields:
     ///     - `addresses`: (json array of string, required) The addresses to get transactions from.
-    ///     - `start`: (numeric, required) The lower height to start looking for transactions (inclusive).
-    ///     - `end`: (numeric, required) The top height to stop looking for transactions (inclusive).
+    ///     - `start`: (numeric, optional) The lower height to start looking for transactions (inclusive).
+    ///     - `end`: (numeric, optional) The top height to stop looking for transactions (inclusive).
     ///
     /// # Notes
     ///
@@ -1451,13 +1454,11 @@ where
         let mut state = self.state.clone();
         let latest_chain_tip = self.latest_chain_tip.clone();
 
-        let start = Height(request.start);
-        let end = Height(request.end);
-
-        let chain_height = best_chain_tip_height(&latest_chain_tip)?;
-
-        // height range checks
-        check_height_range(start, end, chain_height)?;
+        let height_range = build_height_range(
+            request.start,
+            request.end,
+            best_chain_tip_height(&latest_chain_tip)?,
+        )?;
 
         let valid_addresses = AddressStrings {
             addresses: request.addresses,
@@ -1466,7 +1467,7 @@ where
 
         let request = zebra_state::ReadRequest::TransactionIdsByAddresses {
             addresses: valid_addresses,
-            height_range: start..=end,
+            height_range,
         };
         let response = state
             .ready()
@@ -2496,9 +2497,9 @@ pub struct GetAddressTxIdsRequest {
     // A list of addresses to get transactions from.
     addresses: Vec<String>,
     // The height to start looking for transactions.
-    start: u32,
+    start: Option<u32>,
     // The height to end looking for transactions.
-    end: u32,
+    end: Option<u32>,
 }
 
 impl GetAddressTxIdsRequest {
@@ -2506,13 +2507,17 @@ impl GetAddressTxIdsRequest {
     pub fn from_parts(addresses: Vec<String>, start: u32, end: u32) -> Self {
         GetAddressTxIdsRequest {
             addresses,
-            start,
-            end,
+            start: Some(start),
+            end: Some(end),
         }
     }
     /// Returns the contents of [`GetAddressTxIdsRequest`].
     pub fn into_parts(&self) -> (Vec<String>, u32, u32) {
-        (self.addresses.clone(), self.start, self.end)
+        (
+            self.addresses.clone(),
+            self.start.unwrap_or(0),
+            self.end.unwrap_or(0),
+        )
     }
 }
 
@@ -2578,15 +2583,36 @@ impl OrchardTrees {
     }
 }
 
-/// Check if provided height range is valid for address indexes.
-fn check_height_range(start: Height, end: Height, chain_height: Height) -> Result<()> {
-    if start == Height(0) || end == Height(0) {
-        return Err(ErrorObject::owned(
-            ErrorCode::InvalidParams.code(),
-            format!("start {start:?} and end {end:?} must both be greater than zero"),
-            None::<()>,
-        ));
-    }
+/// Build a valid height range from the given optional start and end numbers.
+///
+/// # Parameters
+///
+/// - `start`: Optional starting height. If not provided, defaults to 0.
+/// - `end`: Optional ending height. A value of 0 or absence of a value indicates to use `chain_height`.
+/// - `chain_height`: The maximum permissible height.
+///
+/// # Returns
+///
+/// A `RangeInclusive<Height>` from the clamped start to the clamped end.
+///
+/// # Errors
+///
+/// Returns an error if the computed start is greater than the computed end.
+fn build_height_range(
+    start: Option<u32>,
+    end: Option<u32>,
+    chain_height: Height,
+) -> Result<RangeInclusive<Height>> {
+    // Convert optional values to Height, using 0 (as Height(0)) when missing.
+    // If start is above chain_height, clamp it to chain_height.
+    let start = Height(start.unwrap_or(0)).min(chain_height);
+
+    // For `end`, treat a zero value or missing value as `chain_height`:
+    let end = match end {
+        Some(0) | None => chain_height,
+        Some(val) => Height(val).min(chain_height),
+    };
+
     if start > end {
         return Err(ErrorObject::owned(
             ErrorCode::InvalidParams.code(),
@@ -2594,15 +2620,8 @@ fn check_height_range(start: Height, end: Height, chain_height: Height) -> Resul
             None::<()>,
         ));
     }
-    if start > chain_height || end > chain_height {
-        return Err(ErrorObject::owned(
-            ErrorCode::InvalidParams.code(),
-            format!("start {start:?} and end {end:?} must both be less than or equal to the chain tip {chain_height:?}"),
-            None::<()>,
-        ));
-    }
 
-    Ok(())
+    Ok(start..=end)
 }
 
 /// Given a potentially negative index, find the corresponding `Height`.
diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs
index bad3b85b478..ab32234fccd 100644
--- a/zebra-rpc/src/methods/tests/snapshot.rs
+++ b/zebra-rpc/src/methods/tests/snapshot.rs
@@ -466,8 +466,8 @@ async fn test_rpc_response_data_for_network(network: &Network) {
     let get_address_tx_ids = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses: addresses.clone(),
-            start: 1,
-            end: 10,
+            start: Some(1),
+            end: Some(10),
         })
         .await
         .expect("We should have a vector of strings");
@@ -476,8 +476,8 @@ async fn test_rpc_response_data_for_network(network: &Network) {
     let get_address_tx_ids = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses: addresses.clone(),
-            start: 2,
-            end: 2,
+            start: Some(2),
+            end: Some(2),
         })
         .await
         .expect("We should have a vector of strings");
@@ -486,20 +486,31 @@ async fn test_rpc_response_data_for_network(network: &Network) {
     let get_address_tx_ids = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses: addresses.clone(),
-            start: 3,
-            end: EXCESSIVE_BLOCK_HEIGHT,
+            start: Some(3),
+            end: Some(EXCESSIVE_BLOCK_HEIGHT),
         })
-        .await;
-    snapshot_rpc_getaddresstxids_invalid("excessive_end", get_address_tx_ids, &settings);
+        .await
+        .expect("We should have a vector of strings");
+    snapshot_rpc_getaddresstxids_valid("excessive_end", get_address_tx_ids, &settings);
+
+    let get_address_tx_ids = rpc
+        .get_address_tx_ids(GetAddressTxIdsRequest {
+            addresses: addresses.clone(),
+            start: Some(EXCESSIVE_BLOCK_HEIGHT),
+            end: Some(EXCESSIVE_BLOCK_HEIGHT + 1),
+        })
+        .await
+        .expect("We should have a vector of strings");
+    snapshot_rpc_getaddresstxids_valid("excessive_start", get_address_tx_ids, &settings);
 
     let get_address_tx_ids = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses: addresses.clone(),
-            start: EXCESSIVE_BLOCK_HEIGHT,
-            end: EXCESSIVE_BLOCK_HEIGHT + 1,
+            start: Some(2),
+            end: Some(1),
         })
         .await;
-    snapshot_rpc_getaddresstxids_invalid("excessive_start", get_address_tx_ids, &settings);
+    snapshot_rpc_getaddresstxids_invalid("end_greater_start", get_address_tx_ids, &settings);
 
     // `getaddressutxos`
     let get_address_utxos = rpc
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_end@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_end_greater_start@mainnet_10.snap
similarity index 50%
rename from zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_end@mainnet_10.snap
rename to zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_end_greater_start@mainnet_10.snap
index e2fcae6abca..f68d3d513dd 100644
--- a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_end@mainnet_10.snap
+++ b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_end_greater_start@mainnet_10.snap
@@ -5,6 +5,6 @@ expression: transactions
 {
   "Err": {
     "code": -32602,
-    "message": "start Height(3) and end Height(16777216) must both be less than or equal to the chain tip Height(10)"
+    "message": "start Height(2) must be less than or equal to end Height(1)"
   }
 }
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_end@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_end_greater_start@testnet_10.snap
similarity index 50%
rename from zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_end@testnet_10.snap
rename to zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_end_greater_start@testnet_10.snap
index e2fcae6abca..f68d3d513dd 100644
--- a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_end@testnet_10.snap
+++ b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_end_greater_start@testnet_10.snap
@@ -5,6 +5,6 @@ expression: transactions
 {
   "Err": {
     "code": -32602,
-    "message": "start Height(3) and end Height(16777216) must both be less than or equal to the chain tip Height(10)"
+    "message": "start Height(2) must be less than or equal to end Height(1)"
   }
 }
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_start@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_start@mainnet_10.snap
deleted file mode 100644
index 4256ecc9330..00000000000
--- a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_start@mainnet_10.snap
+++ /dev/null
@@ -1,10 +0,0 @@
----
-source: zebra-rpc/src/methods/tests/snapshot.rs
-expression: transactions
----
-{
-  "Err": {
-    "code": -32602,
-    "message": "start Height(16777216) and end Height(16777217) must both be less than or equal to the chain tip Height(10)"
-  }
-}
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_start@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_start@testnet_10.snap
deleted file mode 100644
index 4256ecc9330..00000000000
--- a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_invalid_excessive_start@testnet_10.snap
+++ /dev/null
@@ -1,10 +0,0 @@
----
-source: zebra-rpc/src/methods/tests/snapshot.rs
-expression: transactions
----
-{
-  "Err": {
-    "code": -32602,
-    "message": "start Height(16777216) and end Height(16777217) must both be less than or equal to the chain tip Height(10)"
-  }
-}
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_end@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_end@mainnet_10.snap
new file mode 100644
index 00000000000..260981d1eb9
--- /dev/null
+++ b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_end@mainnet_10.snap
@@ -0,0 +1,14 @@
+---
+source: zebra-rpc/src/methods/tests/snapshot.rs
+expression: transactions
+---
+[
+  "4e3670eae21d0c7225716abacb3a95c203a75d7601594afddb07d175c3babe83",
+  "781bb9b0168831054c1ed3afd8bf153f3fc848e102cea86977545c86f6e2446e",
+  "9a4adaf3953818eb1634407032db0e00ef2441c49c1364161411d0743ec1a939",
+  "1dd2064423542cfee7ca4ec565acb37e1ad217035b6119668352165b20006fe1",
+  "26b813b0743f015e6b41519d4ca78ee7b5c76cbac5c8b8ac0ec6b9cf88b8328d",
+  "7a772e6fe8e6bbfc34f0a61ba472d9f9dfe3a5539adfdac2f3c6ae659d44de03",
+  "ff24ab621bf3e46ad195b47dca7f11fb376f53e23b2e14a6e9b6a1907cc27cdc",
+  "5f2702708af1d8727ad3f0da3ba74de14019232499c0324ddce236cf97e32548"
+]
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_end@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_end@testnet_10.snap
new file mode 100644
index 00000000000..ea24d0bd539
--- /dev/null
+++ b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_end@testnet_10.snap
@@ -0,0 +1,14 @@
+---
+source: zebra-rpc/src/methods/tests/snapshot.rs
+expression: transactions
+---
+[
+  "4a3bf3f814a3aef93423890c8afa9709229aaf3daf4da98c70d810253d3b9550",
+  "3373ed6deb1130f310d8788db5dfdb92e52980b34ca02ea124ced11aa247f80b",
+  "476480f7c2580a9e39b9d78892fea996c389e6627c8962700563c19b68cc7bee",
+  "23daf8408d825feb09dfeaaceccf1307ed1008265c7145573374872b332c57ab",
+  "47aebd007159819c19519a31bb87b8b40b9b09701fcc0e40bc61c98d283117f2",
+  "29f8982be208c9d8737200f0ecfd3f42c175b7dd67a0aba85812283fc443a443",
+  "9eec40dcf5f72aa0619472cbc3c336229fce2ff983b47ebccc7bf8800759781c",
+  "3887181b326c25e1ea1b6885c9b437280ca3372dc5b67af72423c88c18a1da2e"
+]
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_start@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_start@mainnet_10.snap
new file mode 100644
index 00000000000..2e6800fed11
--- /dev/null
+++ b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_start@mainnet_10.snap
@@ -0,0 +1,7 @@
+---
+source: zebra-rpc/src/methods/tests/snapshot.rs
+expression: transactions
+---
+[
+  "5f2702708af1d8727ad3f0da3ba74de14019232499c0324ddce236cf97e32548"
+]
diff --git a/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_start@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_start@testnet_10.snap
new file mode 100644
index 00000000000..e263425f7f3
--- /dev/null
+++ b/zebra-rpc/src/methods/tests/snapshots/get_address_tx_ids_valid_excessive_start@testnet_10.snap
@@ -0,0 +1,7 @@
+---
+source: zebra-rpc/src/methods/tests/snapshot.rs
+expression: transactions
+---
+[
+  "3887181b326c25e1ea1b6885c9b437280ca3372dc5b67af72423c88c18a1da2e"
+]
diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs
index 3d54f179eb8..70f3d483e7d 100644
--- a/zebra-rpc/src/methods/tests/vectors.rs
+++ b/zebra-rpc/src/methods/tests/vectors.rs
@@ -1,6 +1,5 @@
 //! Fixed test vectors for RPC methods.
 
-use std::ops::RangeInclusive;
 use std::sync::Arc;
 
 use futures::FutureExt;
@@ -1070,8 +1069,8 @@ async fn rpc_getaddresstxids_invalid_arguments() {
     let rpc_rsp = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses: vec!["t1invalidaddress".to_owned()],
-            start: 1,
-            end: 2,
+            start: Some(1),
+            end: Some(2),
         })
         .await
         .unwrap_err();
@@ -1085,8 +1084,8 @@ async fn rpc_getaddresstxids_invalid_arguments() {
     let addresses = vec![address.clone()];
 
     // call the method with start greater than end
-    let start: u32 = 2;
-    let end: u32 = 1;
+    let start: Option<u32> = Some(2);
+    let end: Option<u32> = Some(1);
     let error = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses: addresses.clone(),
@@ -1100,38 +1099,6 @@ async fn rpc_getaddresstxids_invalid_arguments() {
         "start Height(2) must be less than or equal to end Height(1)".to_string()
     );
 
-    // call the method with start equal zero
-    let start: u32 = 0;
-    let end: u32 = 1;
-    let error = rpc
-        .get_address_tx_ids(GetAddressTxIdsRequest {
-            addresses: addresses.clone(),
-            start,
-            end,
-        })
-        .await
-        .unwrap_err();
-    assert_eq!(
-        error.message(),
-        "start Height(0) and end Height(1) must both be greater than zero".to_string()
-    );
-
-    // call the method outside the chain tip height
-    let start: u32 = 1;
-    let end: u32 = 11;
-    let error = rpc
-        .get_address_tx_ids(GetAddressTxIdsRequest {
-            addresses,
-            start,
-            end,
-        })
-        .await
-        .unwrap_err();
-    assert_eq!(
-        error.message(),
-        "start Height(1) and end Height(11) must both be less than or equal to the chain tip Height(10)".to_string()
-    );
-
     mempool.expect_no_requests().await;
 
     // The queue task should continue without errors or panics
@@ -1167,16 +1134,16 @@ async fn rpc_getaddresstxids_response() {
 
         if network == Mainnet {
             // Exhaustively test possible block ranges for mainnet.
-            //
-            // TODO: if it takes too long on slower machines, turn this into a proptest with 10-20 cases
             for start in 1..=10 {
                 for end in start..=10 {
                     rpc_getaddresstxids_response_with(
                         &network,
-                        start..=end,
+                        Some(start),
+                        Some(end),
                         &address,
                         &read_state,
                         &latest_chain_tip,
+                        (end - start + 1) as usize,
                     )
                     .await;
                 }
@@ -1185,22 +1152,87 @@ async fn rpc_getaddresstxids_response() {
             // Just test the full range for testnet.
             rpc_getaddresstxids_response_with(
                 &network,
-                1..=10,
+                Some(1),
+                Some(10),
                 &address,
                 &read_state,
                 &latest_chain_tip,
+                // response should be limited to the chain size.
+                10,
             )
             .await;
         }
+
+        // No range arguments should be equivalent to the full range.
+        rpc_getaddresstxids_response_with(
+            &network,
+            None,
+            None,
+            &address,
+            &read_state,
+            &latest_chain_tip,
+            10,
+        )
+        .await;
+
+        // Range of 0s should be equivalent to the full range.
+        rpc_getaddresstxids_response_with(
+            &network,
+            Some(0),
+            Some(0),
+            &address,
+            &read_state,
+            &latest_chain_tip,
+            10,
+        )
+        .await;
+
+        // Start and and outside of the range should use the chain tip.
+        rpc_getaddresstxids_response_with(
+            &network,
+            Some(11),
+            Some(11),
+            &address,
+            &read_state,
+            &latest_chain_tip,
+            1,
+        )
+        .await;
+
+        // End outside the range should use the chain tip.
+        rpc_getaddresstxids_response_with(
+            &network,
+            None,
+            Some(11),
+            &address,
+            &read_state,
+            &latest_chain_tip,
+            10,
+        )
+        .await;
+
+        // Start outside the range should use the chain tip.
+        rpc_getaddresstxids_response_with(
+            &network,
+            Some(11),
+            None,
+            &address,
+            &read_state,
+            &latest_chain_tip,
+            1,
+        )
+        .await;
     }
 }
 
 async fn rpc_getaddresstxids_response_with(
     network: &Network,
-    range: RangeInclusive<u32>,
+    start: Option<u32>,
+    end: Option<u32>,
     address: &transparent::Address,
     read_state: &ReadStateService,
     latest_chain_tip: &LatestChainTip,
+    expected_response_len: usize,
 ) {
     let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
 
@@ -1223,14 +1255,14 @@ async fn rpc_getaddresstxids_response_with(
     let response = rpc
         .get_address_tx_ids(GetAddressTxIdsRequest {
             addresses,
-            start: *range.start(),
-            end: *range.end(),
+            start,
+            end,
         })
         .await
         .expect("arguments are valid so no error can happen here");
 
     // One founders reward output per coinbase transactions, no other transactions.
-    assert_eq!(response.len(), range.count());
+    assert_eq!(response.len(), expected_response_len);
 
     mempool.expect_no_requests().await;
 


Reviews:
user: login: arya2
body: Thank you!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T14:43:07Z
user: login: arya2
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T12:29:35Z

Review comments:
diff_hunk: @@ -2631,31 +2633,57 @@ impl OrchardTrees {
     }
 }
 
-/// Check if provided height range is valid for address indexes.
-fn check_height_range(start: Height, end: Height, chain_height: Height) -> Result<()> {
-    if start == Height(0) || end == Height(0) {
-        return Err(ErrorObject::owned(
-            ErrorCode::InvalidParams.code(),
-            format!("start {start:?} and end {end:?} must both be greater than zero"),
-            None::<()>,
-        ));
-    }
+/// Build a valid height range from the given optional start and end numbers.
+///
+/// # Parameters
+///
+/// - `start`: Optional starting height. If not provided, defaults to 0.
+/// - `end`: Optional ending height. A value of 0 or absence of a value indicates to use `chain_height`.
+/// - `chain_height`: The maximum permissible height.
+///
+/// # Returns
+///
+/// A `RangeInclusive<Height>` from the clamped start to the clamped end.
+///
+/// # Errors
+///
+/// Returns an error if the computed start is greater than the computed end.
+fn build_height_range(
+    start: Option<u32>,
+    end: Option<u32>,
+    chain_height: Height,
+) -> Result<RangeInclusive<Height>> {
+    // Convert optional values to Height, using 0 (as Height(0)) when missing.
+    let start = start.map(Height).unwrap_or(Height(0));
+    // If start is above chain_height, clamp it to chain_height.
+    let start = if start > chain_height {
+        chain_height
+    } else {
+        start
+    };
path: zebra-rpc/src/methods.rs
user: login: arya2
body: Nitpick/Optional/Subjective:

```suggestion
    // Convert optional values to Height, using 0 (as Height(0)) when missing.
    // If start is above chain_height, clamp it to chain_height.
    let start = Height(start.unwrap_or(0)).min(chain_height);
```

```
created_at: 2025-04-10T14:28:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2631,31 +2633,57 @@ impl OrchardTrees {
     }
 }
 
-/// Check if provided height range is valid for address indexes.
-fn check_height_range(start: Height, end: Height, chain_height: Height) -> Result<()> {
-    if start == Height(0) || end == Height(0) {
-        return Err(ErrorObject::owned(
-            ErrorCode::InvalidParams.code(),
-            format!("start {start:?} and end {end:?} must both be greater than zero"),
-            None::<()>,
-        ));
-    }
+/// Build a valid height range from the given optional start and end numbers.
+///
+/// # Parameters
+///
+/// - `start`: Optional starting height. If not provided, defaults to 0.
+/// - `end`: Optional ending height. A value of 0 or absence of a value indicates to use `chain_height`.
+/// - `chain_height`: The maximum permissible height.
+///
+/// # Returns
+///
+/// A `RangeInclusive<Height>` from the clamped start to the clamped end.
+///
+/// # Errors
+///
+/// Returns an error if the computed start is greater than the computed end.
+fn build_height_range(
+    start: Option<u32>,
+    end: Option<u32>,
+    chain_height: Height,
+) -> Result<RangeInclusive<Height>> {
+    // Convert optional values to Height, using 0 (as Height(0)) when missing.
+    let start = start.map(Height).unwrap_or(Height(0));
+    // If start is above chain_height, clamp it to chain_height.
+    let start = if start > chain_height {
+        chain_height
+    } else {
+        start
+    };
+
+    // For `end`, treat a zero value or missing value as `chain_height`:
+    let end = match end {
+        Some(0) | None => chain_height,
+        Some(val) => {
+            let val_height = Height(val);
+            if val_height > chain_height {
+                chain_height
+            } else {
+                val_height
+            }
+        }
path: zebra-rpc/src/methods.rs
user: login: arya2
body: Nitpick/Optional/Subjective:

```suggestion
        Some(val) => Height(val).min(chain_height),
```
created_at: 2025-04-10T14:30:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1104,22 +1071,86 @@ async fn rpc_getaddresstxids_response() {
             // Just test the full range for testnet.
             rpc_getaddresstxids_response_with(
                 &network,
-                1..=10,
+                Some(1),
+                Some(10),
                 &address,
                 &read_state,
                 &latest_chain_tip,
+                10,
path: zebra-rpc/src/methods/tests/vectors.rs
user: login: arya2
body: Nitpick/Optional:

```suggestion
                // response should be limited to the chain size.
                10,
```
created_at: 2025-04-10T14:42:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: mpguerra
created_at: 2025-04-14T11:00:20Z
body: @LarryRuane just checking that you've seen this and it matches the fix for https://github.com/zcash/zcash/pull/6932
author_association: CONTRIBUTOR
user: login: LarryRuane
created_at: 2025-04-15T05:47:34Z
body: > @LarryRuane just checking that you've seen this and it matches the fix for zcash/zcash#6932

I verified this experimentally (and also looked at the code briefly, and single-stepped through it). After starting the latest `zebrad` main branch (that includes this PR), I ran the following, which is the same as https://github.com/zcash/zcash/pull/6932#issuecomment-2301084937 except using `curl` instead of `zcash-cli`:
```
$ curl -s --data-binary '{"jsonrpc": "1.0", "id": "", "method": "getaddresstxids", "params": [{"addresses": ["t1aY1Rc7rm5TRbXJzCxgtBhDeyN7q1bZWVm"], "start":997000, "end":997311}]}' -H 'content-type: application/json' http://127.0.0.1:8232/ | jq '.result[]' |
while read txid
do
    curl -s --data-binary '{"jsonrpc": "1.0", "id": "", "method": "getrawtransaction", "params": ['$txid',2]}' -H 'content-type: application/json' http://127.0.0.1:8232/ | jq .result.height
done
997044
997074
997310
```
(These are the heights of blocks containing transactions referencing this address.) 

I ran this again with various combinations of `start` and `end` not specified and specified as zero, and in all cases, 37 results were returned, which matches the earlier `zcashd` testing in that [PR](https://github.com/zcash/zcash/pull/6932), which is the full set across the entire blockchain history. I also tried various start and end values, including boundary cases, and setting `end` beyond the current height. I always saw the expected results, including that they were sorted by block height.

So I'm confident it's working correctly.
author_association: NONE