zcash_keys/keys/transparent/gap_limits.rs
1//! Types and operations specific to generation of transparent addresses using a "gap limit"
2//! strategy, as described by [`BIP 44`].
3//!
4//! [`BIP 44`]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
5
6use alloc::vec::Vec;
7use core::hash::Hash;
8use core::ops::Range;
9
10use crate::{
11 address::Address,
12 keys::{
13 AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey,
14 UnifiedIncomingViewingKey,
15 },
16};
17use transparent::{
18 address::TransparentAddress,
19 keys::{IncomingViewingKey, NonHardenedChildIndex, NonHardenedChildRange, TransparentKeyScope},
20};
21use zcash_address::unified::Typecode;
22use zip32::DiversifierIndex;
23
24/// Configuration for gap limits used in transparent address management.
25///
26/// A "gap limit" is the number of consecutive unused addresses that the wallet will generate
27/// before stopping. This concept comes from BIP-44 HD wallet standards: when scanning for
28/// funds, the wallet generates addresses sequentially and stops when it encounters a "gap"
29/// of unused addresses equal to this limit. In recovery, a wallet will discover funds
30/// belonging to generated addresses. Note that order of operations matters; if a wallet scans
31/// blocks for transparent outputs instead of querying the UTXO set, out-of-order scanning may
32/// result in funds not being found; as a result, wallets should still fall back to making
33/// temporally unlinkable, private queries to the UTXO set for each address controlled by the
34/// wallet at any time that a gap in scanned blocks is introduced by out-of-order scanning.
35///
36/// In Zcash, we define individual gap limits for the following address types:
37/// - **External addresses**: Addresses shared with external parties.
38/// - **Internal (change) addresses**: Used for transparent change outputs.
39/// - **Ephemeral addresses**: Used for single-use purposes like ZIP-320 TEX address transfers.
40#[derive(Debug, Copy, Clone, PartialEq, Eq)]
41pub struct GapLimits {
42 external: u32,
43 internal: u32,
44 ephemeral: u32,
45}
46
47impl GapLimits {
48 /// Constructs a new `GapLimits` value from its constituent parts.
49 ///
50 /// The gap limits recommended for use with this crate are supplied by the [`Default`]
51 /// implementation for this type.
52 ///
53 /// This constructor is only available under the `unstable` feature, as it is not recommended
54 /// for general use.
55 #[cfg(any(test, feature = "test-dependencies", feature = "unstable"))]
56 pub fn new(external: u32, internal: u32, ephemeral: u32) -> Self {
57 Self {
58 external,
59 internal,
60 ephemeral,
61 }
62 }
63
64 /// Returns the gap limit for external-scoped addresses.
65 pub fn external(&self) -> u32 {
66 self.external
67 }
68
69 /// Returns the gap limit for internal-scoped addresses.
70 pub fn internal(&self) -> u32 {
71 self.internal
72 }
73
74 /// Returns the gap limit for ephemeral-scoped addresses.
75 pub fn ephemeral(&self) -> u32 {
76 self.ephemeral
77 }
78
79 /// Returns the gap limit for the given transparent key scope, or `None` if the key scope is
80 /// one for which gap limits are not managed by this type.
81 pub fn limit_for(&self, scope: TransparentKeyScope) -> Option<u32> {
82 match scope {
83 TransparentKeyScope::EXTERNAL => Some(self.external()),
84 TransparentKeyScope::INTERNAL => Some(self.internal()),
85 TransparentKeyScope::EPHEMERAL => Some(self.ephemeral()),
86 _ => None,
87 }
88 }
89}
90
91/// The default gap limits supported by this implementation are:
92///
93/// - external addresses: 10
94/// - transparent internal (change) addresses: 5
95/// - ephemeral addresses: 10
96///
97/// These limits are chosen with the following rationale:
98/// - At present, many wallets query light wallet servers with a set of addresses, because querying
99/// for each address independently and in a fashion that is not susceptible to clustering via
100/// timing correlation leads to undesirable delays in discovery of received funds. As such, it is
101/// desirable to minimize the number of addresses that can be "linked", i.e. understood by the
102/// light wallet server to all belong to the same wallet.
103/// - For transparent change addresses it is always expected that an address will receive funds
104/// immediately following its generation except in the case of wallet failure.
105/// - For externally-scoped transparent addresses and ephemeral addresses, it is desirable to use a
106/// slightly larger gap limit to account for addresses that were shared with counterparties never
107/// having been used. However, we don't want to use the full 20-address gap limit space because
108/// it's possible that in the future, changes to the light wallet protocol will obviate the need to
109/// query for UTXOs in a fashion that links those addresses to one another. In such a
110/// circumstance, the gap limit will be adjusted upward and address rotation should then choose
111/// an address that is outside the current gap limit; after that change, newly generated
112/// addresses will not be exposed as linked in the view of the light wallet server.
113impl Default for GapLimits {
114 fn default() -> Self {
115 Self {
116 external: 10,
117 internal: 5,
118 ephemeral: 10,
119 }
120 }
121}
122
123/// A trait providing wallet storage operations required for transparent address gap limit
124/// management.
125///
126/// Implementations of this trait allow the gap limit logic in [`generate_gap_addresses`] to query
127/// and update the wallet's transparent address state without being coupled to a specific storage
128/// backend.
129pub trait AddressStore {
130 /// The type of errors produced by the wallet storage backend.
131 type Error;
132
133 /// A wallet-internal account identifier.
134 type AccountRef: Copy + Eq + Hash;
135
136 /// Returns the transparent address index at the start of the first gap of at least `gap_limit`
137 /// indices in the given account, considering only addresses derived for the specified key scope.
138 ///
139 /// Returns `Ok(None)` if the gap would start at an index greater than the maximum valid
140 /// non-hardened transparent child index.
141 fn find_gap_start(
142 &self,
143 account_ref: Self::AccountRef,
144 key_scope: TransparentKeyScope,
145 gap_limit: u32,
146 ) -> Result<Option<NonHardenedChildIndex>, Self::Error>;
147
148 /// Persists a range of derived transparent addresses to the wallet storage.
149 ///
150 /// Each entry in the list contains the wallet-level address, the raw transparent address,
151 /// and the child index from which the address was derived.
152 fn store_address_range(
153 &mut self,
154 account_id: Self::AccountRef,
155 key_scope: TransparentKeyScope,
156 list: Vec<(Address, TransparentAddress, NonHardenedChildIndex)>,
157 ) -> Result<(), Self::Error>;
158}
159
160fn generate_external_address(
161 uivk: &UnifiedIncomingViewingKey,
162 ua_request: UnifiedAddressRequest,
163 index: NonHardenedChildIndex,
164) -> Result<(Address, TransparentAddress), AddressGenerationError> {
165 let transparent_address = uivk
166 .p2pkh()
167 .ok_or(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH))?
168 .derive_address(index)
169 .map_err(|_| {
170 AddressGenerationError::InvalidTransparentChildIndex(DiversifierIndex::from(index))
171 })?;
172
173 // A key that has nothing but a transparent item now yields a transparent-only unified
174 // address, so `NoSatisfiableReceiver` here means that the key has a data item that this
175 // build cannot interpret, or that the request names a pool the key does not have. Emitting
176 // a bare transparent address in either case would silently discard receiving capability
177 // that the recipient has, so the error propagates.
178 let ua = uivk.address(index.into(), ua_request)?;
179
180 Ok((Address::from(ua), transparent_address))
181}
182
183/// Generates a list of addresses for the given range of transparent child indices.
184///
185/// For external-scoped addresses, a unified address is generated using the provided
186/// [`UnifiedAddressRequest`]; for internal and ephemeral scopes, the raw transparent address is
187/// returned.
188///
189/// Returns an empty list if the account lacks a transparent key and `require_key` is `false`.
190/// Returns an error if the key is required but unavailable, or if the key scope is unsupported.
191pub fn generate_address_list(
192 account_uivk: &UnifiedIncomingViewingKey,
193 account_ufvk: Option<&UnifiedFullViewingKey>,
194 key_scope: TransparentKeyScope,
195 request: UnifiedAddressRequest,
196 range_to_store: Range<NonHardenedChildIndex>,
197 require_key: bool,
198) -> Result<Vec<(Address, TransparentAddress, NonHardenedChildIndex)>, AddressGenerationError> {
199 let account_pubkey = if let Some(k) = account_ufvk.and_then(|ufvk| ufvk.p2pkh()) {
200 k
201 } else if matches!(
202 key_scope,
203 TransparentKeyScope::INTERNAL | TransparentKeyScope::EPHEMERAL
204 ) && require_key
205 {
206 return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH));
207 } else {
208 // No addresses to generate
209 return Ok(vec![]);
210 };
211
212 let gen_addrs = |key_scope: TransparentKeyScope, index: NonHardenedChildIndex| match key_scope {
213 TransparentKeyScope::EXTERNAL => generate_external_address(account_uivk, request, index),
214 TransparentKeyScope::INTERNAL => {
215 let internal_address = account_pubkey
216 .derive_internal_ivk()?
217 .derive_address(index)?;
218 Ok((Address::from(internal_address), internal_address))
219 }
220 TransparentKeyScope::EPHEMERAL => {
221 let ephemeral_address = account_pubkey
222 .derive_ephemeral_ivk()?
223 .derive_ephemeral_address(index)?;
224 Ok((Address::from(ephemeral_address), ephemeral_address))
225 }
226 _ => Err(AddressGenerationError::UnsupportedTransparentKeyScope(
227 key_scope,
228 )),
229 };
230
231 NonHardenedChildRange::from(range_to_store)
232 .into_iter()
233 .map(|transparent_child_index| {
234 let (address, transparent_address) = gen_addrs(key_scope, transparent_child_index)?;
235 Ok((address, transparent_address, transparent_child_index))
236 })
237 .collect::<Result<Vec<_>, _>>()
238}
239
240/// Errors that can occur when generating transparent gap addresses.
241#[derive(Debug)]
242pub enum GapAddressesError<SE> {
243 /// An error occurred in the underlying wallet storage backend.
244 Storage(SE),
245 /// An error occurred while deriving a transparent address.
246 AddressGeneration(AddressGenerationError),
247 /// The specified account was not found in the wallet database.
248 AccountUnknown,
249}
250
251/// Generates transparent addresses to fill the gap for a given account and key scope.
252///
253/// This function queries the wallet backend (via [`AddressStore`]) to find the start
254/// of the first gap of unused addresses, then generates enough addresses to maintain the
255/// configured gap limit. If no gap exists (i.e., the address space is exhausted), this is a
256/// no-op.
257#[allow(clippy::too_many_arguments)]
258pub fn generate_gap_addresses<DbT, SE>(
259 wallet_db: &mut DbT,
260 gap_limits: &GapLimits,
261 account_id: DbT::AccountRef,
262 account_uivk: &UnifiedIncomingViewingKey,
263 account_ufvk: Option<&UnifiedFullViewingKey>,
264 key_scope: TransparentKeyScope,
265 request: UnifiedAddressRequest,
266 require_key: bool,
267) -> Result<(), GapAddressesError<SE>>
268where
269 DbT: AddressStore<Error = SE>,
270{
271 let gap_limit = gap_limits
272 .limit_for(key_scope)
273 .ok_or(GapAddressesError::AddressGeneration(
274 AddressGenerationError::UnsupportedTransparentKeyScope(key_scope),
275 ))?;
276
277 if let Some(gap_start) = wallet_db
278 .find_gap_start(account_id, key_scope, gap_limit)
279 .map_err(GapAddressesError::Storage)?
280 {
281 let address_list = generate_address_list(
282 account_uivk,
283 account_ufvk,
284 key_scope,
285 request,
286 gap_start..gap_start.saturating_add(gap_limit),
287 require_key,
288 )
289 .map_err(GapAddressesError::AddressGeneration)?;
290 wallet_db
291 .store_address_range(account_id, key_scope, address_list)
292 .map_err(GapAddressesError::Storage)?;
293 }
294
295 Ok(())
296}