Skip to main content

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}