Merge pull request #1755 from nuttycom/fix/transparent_address_migration

zcash_client_sqlite: Fix migration error caused by missing transparent addresses prior to the index of the default address
This commit is contained in:
Kris Nuttycombe 2025-04-02 18:26:38 -06:00 committed by GitHub
commit 2b89d80f12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 377 additions and 133 deletions

4
Cargo.lock generated
View File

@ -6231,7 +6231,7 @@ dependencies = [
[[package]]
name = "zcash_client_sqlite"
version = "0.16.1"
version = "0.16.2"
dependencies = [
"ambassador",
"assert_matches",
@ -6456,7 +6456,7 @@ dependencies = [
[[package]]
name = "zcash_transparent"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"bip32",
"blake2b_simd",

View File

@ -295,11 +295,11 @@ user-login = "nuttycom"
user-name = "Kris Nuttycombe"
[[publisher.zcash_client_sqlite]]
version = "0.16.1"
when = "2025-03-27"
user-id = 6289
user-login = "str4d"
user-name = "Jack Grigg"
version = "0.16.2"
when = "2025-04-03"
user-id = 169181
user-login = "nuttycom"
user-name = "Kris Nuttycombe"
[[publisher.zcash_encoding]]
version = "0.3.0"
@ -358,8 +358,8 @@ user-login = "daira"
user-name = "Daira-Emma Hopwood"
[[publisher.zcash_transparent]]
version = "0.2.1"
when = "2025-03-21"
version = "0.2.2"
when = "2025-04-03"
user-id = 169181
user-login = "nuttycom"
user-name = "Kris Nuttycombe"

View File

@ -7,6 +7,14 @@ and this library adheres to Rust's notion of
## [Unreleased]
## [0.16.2] - 2025-04-02
### Fixed
- This release fixes a migration error that could cause some wallets
to crash on startup due to an attempt to associate a received transparent
output with an address that does not exist in the wallet's `addresses`
table.
## [0.16.1] - 2025-03-26
### Fixed

View File

@ -1,7 +1,7 @@
[package]
name = "zcash_client_sqlite"
description = "An SQLite-based Zcash light client"
version = "0.16.1"
version = "0.16.2"
authors = [
"Jack Grigg <jack@z.cash>",
"Kris Nuttycombe <kris@electriccoin.co>"

View File

@ -611,6 +611,20 @@ pub(crate) fn add_account<P: consensus::Parameters>(
false,
)?;
// Pre-generate external transparent addresses prior to the index of the default address.
#[cfg(feature = "transparent-inputs")]
if let Ok(default_addr_idx) = NonHardenedChildIndex::try_from(d_idx) {
transparent::generate_address_range(
conn,
params,
account_id,
KeyScope::EXTERNAL,
UnifiedAddressRequest::ALLOW_ALL,
NonHardenedChildIndex::const_from_index(0)..default_addr_idx,
false,
)?
}
// Pre-generate transparent addresses up to the gap limits for the external, internal,
// and ephemeral key scopes.
#[cfg(feature = "transparent-inputs")]

View File

@ -3,6 +3,7 @@ mod add_account_uuids;
mod add_transaction_views;
mod add_utxo_account;
mod addresses_table;
mod ensure_default_transparent_address;
mod ensure_orchard_ua_receiver;
mod ephemeral_addresses;
mod fix_bad_change_flagging;
@ -99,6 +100,8 @@ pub(super) fn all_migrations<
// fix_bad_change_flagging v_transactions_additional_totals
// |
// transparent_gap_limit_handling
// |
// ensure_default_transparent_address
let rng = Rc::new(Mutex::new(rng));
vec![
Box::new(initial_setup::Migration {}),
@ -169,6 +172,9 @@ pub(super) fn all_migrations<
_clock: clock.clone(),
_rng: rng.clone(),
}),
Box::new(ensure_default_transparent_address::Migration {
_params: params.clone(),
}),
]
}
@ -180,7 +186,7 @@ pub(super) fn all_migrations<
#[allow(dead_code)]
const PUBLIC_MIGRATION_STATES: &[&[Uuid]] = &[
V_0_4_0, V_0_6_0, V_0_8_0, V_0_9_0, V_0_10_0, V_0_10_3, V_0_11_0, V_0_11_1, V_0_11_2, V_0_12_0,
V_0_13_0, V_0_14_0, V_0_15_0,
V_0_13_0, V_0_14_0, V_0_15_0, V_0_16_0, V_0_16_2,
];
/// Leaf migrations in the 0.4.0 release.
@ -252,6 +258,10 @@ const V_0_15_0: &[Uuid] = &[
v_transactions_additional_totals::MIGRATION_ID,
];
const V_0_16_0: &[Uuid] = &[transparent_gap_limit_handling::MIGRATION_ID];
const V_0_16_2: &[Uuid] = &[ensure_default_transparent_address::MIGRATION_ID];
pub(super) fn verify_network_compatibility<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,

View File

@ -0,0 +1,50 @@
//! Ensures that an external transparent address exists in the `addresses` table for each
//! non-hardened child index starting at index 0 and ending at the index corresponding to default
//! address for the account.
use std::collections::HashSet;
use uuid::Uuid;
use rusqlite::Transaction;
use schemerz_rusqlite::RusqliteMigration;
use zcash_protocol::consensus;
use super::transparent_gap_limit_handling;
use crate::wallet::init::WalletMigrationError;
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x702cf97b_8395_4edc_b584_5c9f87f0ef35);
const DEPENDENCIES: &[Uuid] = &[transparent_gap_limit_handling::MIGRATION_ID];
pub(super) struct Migration<P> {
pub(super) _params: P,
}
impl<P> schemerz::Migration<Uuid> for Migration<P> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
DEPENDENCIES.iter().copied().collect()
}
fn description(&self) -> &'static str {
"Ensures the existence of transparent addresses in the range 0..<default_address_idx>"
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, _conn: &Transaction) -> Result<(), WalletMigrationError> {
#[cfg(feature = "transparent-inputs")]
transparent_gap_limit_handling::insert_initial_transparent_addrs(_conn, &self._params)?;
Ok(())
}
fn down(&self, _: &Transaction) -> Result<(), WalletMigrationError> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}

View File

@ -26,7 +26,7 @@ use {
crate::{
wallet::{
encoding::{decode_diversifier_index_be, encode_diversifier_index_be, epoch_seconds},
transparent::{generate_gap_addresses, next_check_time},
transparent::{generate_address_range, generate_gap_addresses, next_check_time},
},
GapLimits,
},
@ -60,6 +60,77 @@ impl<P, C, R> schemerz::Migration<Uuid> for Migration<P, C, R> {
}
}
// For each account, ensure that all diversifier indexes prior to that for the default
// address have corresponding cached transparent addresses.
#[cfg(feature = "transparent-inputs")]
pub(super) fn insert_initial_transparent_addrs<P: consensus::Parameters>(
conn: &rusqlite::Transaction,
params: &P,
) -> Result<(), WalletMigrationError> {
let mut min_addr_diversifiers = conn.prepare(
r#"
SELECT accounts.id AS account_id,
MIN(addresses.transparent_child_index) AS transparent_child_index,
MIN(addresses.diversifier_index_be) AS diversifier_index_be
FROM accounts
LEFT OUTER JOIN addresses
ON accounts.id = addresses.account_id
AND addresses.key_scope = :key_scope_external
GROUP BY accounts.id
"#,
)?;
let mut min_addr_rows = min_addr_diversifiers.query(named_params![
":key_scope_external": KeyScope::EXTERNAL.encode()
])?;
while let Some(row) = min_addr_rows.next()? {
let account_id = AccountRef(row.get::<_, u32>("account_id")?);
let min_transparent_idx = row
.get::<_, Option<u32>>("transparent_child_index")?
.map(|i| {
NonHardenedChildIndex::from_index(i).ok_or(WalletMigrationError::CorruptedData(
format!("{} is not a valid transparent child index.", i),
))
})
.transpose()?;
let min_diversifier_idx = row
.get::<_, Option<Vec<u8>>>("diversifier_index_be")?
.map(|b| decode_diversifier_index_be(&b[..]))
.transpose()?
.and_then(|di| NonHardenedChildIndex::try_from(di).ok());
// Ensure that there is an address for each possible external address index prior to the
// default UA for the account. If the default address has a diversifier index greater than
// the gap limit, we generate transparent addresses up to that index but not beyond.
let start = NonHardenedChildIndex::const_from_index(0);
let end = std::cmp::min(
min_transparent_idx
.or(min_diversifier_idx)
// guarantee that we have an entry at index 0; this address will have previously
// been provided explicitly as one of the wallet's addresses in response to a call
// to `get_transparent_receivers, even if we have no other addresses generated
// (which shouldn't ordinarily be the case anyway)
.unwrap_or(NonHardenedChildIndex::const_from_index(1)),
NonHardenedChildIndex::from_index(GapLimits::default().external())
.expect("default external gap limit fits in non-hardened child index space."),
);
generate_address_range(
conn,
params,
account_id,
KeyScope::EXTERNAL,
UnifiedAddressRequest::ALLOW_ALL,
start..end,
false,
)?;
}
Ok(())
}
impl<P: consensus::Parameters, C: Clock, R: RngCore> RusqliteMigration for Migration<P, C, R> {
type Error = WalletMigrationError;
@ -267,7 +338,7 @@ impl<P: consensus::Parameters, C: Clock, R: RngCore> RusqliteMigration for Migra
.ok()
.and_then(NonHardenedChildIndex::from_index)
.ok_or(WalletMigrationError::CorruptedData(
"ephermeral address indices must be in the range of `u31`"
"ephemeral address indices must be in the range of `u31`"
.to_owned(),
))?
.index(),
@ -352,6 +423,9 @@ impl<P: consensus::Parameters, C: Clock, R: RngCore> RusqliteMigration for Migra
"#,
)?;
#[cfg(feature = "transparent-inputs")]
insert_initial_transparent_addrs(conn, &self.params)?;
// Add foreign key references from the *_received_{notes|outputs} tables to the addresses
// table to make it possible to identify which address was involved. These foreign key
// columns must be nullable as for shielded account-internal. Ideally the foreign key

View File

@ -1,4 +1,5 @@
//! Functions for transparent input support in the wallet.
use core::ops::Range;
use std::collections::{HashMap, HashSet};
use std::num::TryFromIntError;
use std::ops::DerefMut;
@ -12,7 +13,8 @@ use rusqlite::types::Value;
use rusqlite::OptionalExtension;
use rusqlite::{named_params, Connection, Row};
use ::transparent::{
use transparent::keys::NonHardenedChildRange;
use transparent::{
address::{Script, TransparentAddress},
bundle::{OutPoint, TxOut},
keys::{IncomingViewingKey, NonHardenedChildIndex},
@ -25,6 +27,7 @@ use zcash_client_backend::{
},
wallet::{TransparentAddressMetadata, WalletTransparentOutput},
};
use zcash_keys::keys::UnifiedIncomingViewingKey;
use zcash_keys::{
address::Address,
encoding::AddressCodec,
@ -36,7 +39,7 @@ use zcash_protocol::{
value::{ZatBalance, Zatoshis},
TxId,
};
use zip32::Scope;
use zip32::{DiversifierIndex, Scope};
use super::encoding::{decode_epoch_seconds, ReceiverFlags};
use super::{
@ -102,10 +105,11 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
// Get all addresses with the provided scopes.
let mut addr_query = conn.prepare(
"SELECT address, diversifier_index_be, key_scope
"SELECT cached_transparent_receiver_address, transparent_child_index, key_scope
FROM addresses
JOIN accounts ON accounts.id = addresses.account_id
WHERE accounts.uuid = :account_uuid
AND cached_transparent_receiver_address IS NOT NULL
AND key_scope IN rarray(:scopes_ptr)",
)?;
@ -117,30 +121,28 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
])?;
while let Some(row) = rows.next()? {
let ua_str: String = row.get(0)?;
let di_vec: Vec<u8> = row.get(1)?;
let addr_str: String = row.get(0)?;
let address_index: u32 = row.get(1)?;
let address_index = NonHardenedChildIndex::from_index(address_index).ok_or(
SqliteClientError::CorruptedData(format!(
"{} is not a valid transparent child index",
address_index
)),
)?;
let scope = KeyScope::decode(row.get(2)?)?;
let taddr = Address::decode(params, &ua_str)
let taddr = Address::decode(params, &addr_str)
.ok_or_else(|| {
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
})?
.to_transparent_address();
if let Some(taddr) = taddr {
let address_index = address_index_from_diversifier_index_be(&di_vec)?;
let metadata = TransparentAddressMetadata::new(scope.into(), address_index);
ret.insert(taddr, Some(metadata));
}
}
if let Some((taddr, address_index)) =
get_legacy_transparent_address(params, conn, account_uuid)?
{
let metadata = TransparentAddressMetadata::new(KeyScope::EXTERNAL.into(), address_index);
ret.insert(taddr, Some(metadata));
}
Ok(ret)
}
@ -148,7 +150,7 @@ pub(crate) fn uivk_legacy_transparent_address<P: consensus::Parameters>(
params: &P,
uivk_str: &str,
) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
use ::transparent::keys::ExternalIvk;
use transparent::keys::ExternalIvk;
use zcash_address::unified::{Container as _, Encoding as _};
let (network, uivk) = Uivk::decode(uivk_str)
@ -401,6 +403,133 @@ pub(crate) fn reserve_next_n_addresses<P: consensus::Parameters>(
Ok(addresses_to_reserve)
}
pub(crate) fn generate_external_address(
uivk: &UnifiedIncomingViewingKey,
ua_request: UnifiedAddressRequest,
index: NonHardenedChildIndex,
) -> Result<(Address, TransparentAddress), AddressGenerationError> {
let ua = uivk.address(index.into(), ua_request);
let transparent_address = uivk
.transparent()
.as_ref()
.ok_or(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh))?
.derive_address(index)
.map_err(|_| {
AddressGenerationError::InvalidTransparentChildIndex(DiversifierIndex::from(index))
})?;
Ok((
ua.map_or_else(
|e| {
if matches!(e, AddressGenerationError::ShieldedReceiverRequired) {
// fall back to the transparent-only address
Ok(Address::from(transparent_address))
} else {
// other address generation errors are allowed to propagate
Err(e)
}
},
|addr| Ok(Address::from(addr)),
)?,
transparent_address,
))
}
/// Generates addresses to fill the specified non-hardened child index range.
///
/// The provided [`UnifiedAddressRequest`] is used to pre-generate unified addresses that correspond
/// to each transparent address index in question; such unified addresses need not internally
/// contain a transparent receiver, and may be overwritten when these addresses are exposed via the
/// [`WalletWrite::get_next_available_address`] or [`WalletWrite::get_address_for_index`] methods.
/// If no request is provided, each address so generated will contain a receiver for each possible
/// pool: i.e., a recevier for each data item in the account's UFVK or UIVK where the transparent
/// child index is valid.
///
/// [`WalletWrite::get_next_available_address`]: zcash_client_backend::data_api::WalletWrite::get_next_available_address
/// [`WalletWrite::get_address_for_index`]: zcash_client_backend::data_api::WalletWrite::get_address_for_index
pub(crate) fn generate_address_range<P: consensus::Parameters>(
conn: &rusqlite::Transaction,
params: &P,
account_id: AccountRef,
key_scope: KeyScope,
request: UnifiedAddressRequest,
range_to_store: Range<NonHardenedChildIndex>,
require_key: bool,
) -> Result<(), SqliteClientError> {
let account = get_account_internal(conn, params, account_id)?
.ok_or_else(|| SqliteClientError::AccountUnknown)?;
if !account.uivk().has_transparent() {
if require_key {
return Err(SqliteClientError::AddressGeneration(
AddressGenerationError::KeyNotAvailable(Typecode::P2pkh),
));
} else {
return Ok(());
}
}
let gen_addrs = |key_scope: KeyScope, index: NonHardenedChildIndex| {
Ok::<_, SqliteClientError>(match key_scope {
KeyScope::Zip32(zip32::Scope::External) => {
generate_external_address(&account.uivk(), request, index)?
}
KeyScope::Zip32(zip32::Scope::Internal) => {
let internal_address = account
.ufvk()
.and_then(|k| k.transparent())
.expect("presence of transparent key was checked above.")
.derive_internal_ivk()?
.derive_address(index)?;
(Address::from(internal_address), internal_address)
}
KeyScope::Ephemeral => {
let ephemeral_address = account
.ufvk()
.and_then(|k| k.transparent())
.expect("presence of transparent key was checked above.")
.derive_ephemeral_ivk()?
.derive_ephemeral_address(index)?;
(Address::from(ephemeral_address), ephemeral_address)
}
})
};
// exposed_at_height is initially NULL
let mut stmt_insert_address = conn.prepare_cached(
"INSERT INTO addresses (
account_id, diversifier_index_be, key_scope, address,
transparent_child_index, cached_transparent_receiver_address,
receiver_flags
)
VALUES (
:account_id, :diversifier_index_be, :key_scope, :address,
:transparent_child_index, :transparent_address,
:receiver_flags
)
ON CONFLICT (account_id, diversifier_index_be, key_scope) DO NOTHING",
)?;
for transparent_child_index in NonHardenedChildRange::from(range_to_store) {
let (address, transparent_address) = gen_addrs(key_scope, transparent_child_index)?;
let zcash_address = address.to_zcash_address(params);
let receiver_flags: ReceiverFlags = zcash_address
.clone()
.convert::<ReceiverFlags>()
.expect("address is valid");
stmt_insert_address.execute(named_params![
":account_id": account_id.0,
":diversifier_index_be": encode_diversifier_index_be(transparent_child_index.into()),
":key_scope": key_scope.encode(),
":address": zcash_address.encode(),
":transparent_child_index": transparent_child_index.index(),
":transparent_address": transparent_address.encode(params),
":receiver_flags": receiver_flags.bits()
])?;
}
Ok(())
}
/// Extend the range of preallocated addresses in an account to ensure that a full `gap_limit` of
/// transparent addresses is available from the first gap in existing indices of addresses at which
/// a received transaction has been observed on the chain, for each key scope.
@ -424,72 +553,6 @@ pub(crate) fn generate_gap_addresses<P: consensus::Parameters>(
request: UnifiedAddressRequest,
require_key: bool,
) -> Result<(), SqliteClientError> {
let account = get_account_internal(conn, params, account_id)?
.ok_or_else(|| SqliteClientError::AccountUnknown)?;
if !account.uivk().has_transparent() {
if require_key {
return Err(SqliteClientError::AddressGeneration(
AddressGenerationError::KeyNotAvailable(Typecode::P2pkh),
));
} else {
return Ok(());
}
}
let gen_addrs = |key_scope: KeyScope, index: NonHardenedChildIndex| {
Ok::<_, SqliteClientError>(match key_scope {
KeyScope::Zip32(zip32::Scope::External) => {
let ua = account.uivk().address(index.into(), request);
let transparent_address = account
.uivk()
.transparent()
.as_ref()
.expect("presence of transparent key was checked above.")
.derive_address(index)?;
(
ua.map_or_else(
|e| {
if matches!(e, AddressGenerationError::ShieldedReceiverRequired) {
// fall back to the transparent-only address
Ok(Address::from(transparent_address).to_zcash_address(params))
} else {
// other address generation errors are allowed to propagate
Err(e)
}
},
|addr| Ok(Address::from(addr).to_zcash_address(params)),
)?,
transparent_address,
)
}
KeyScope::Zip32(zip32::Scope::Internal) => {
let internal_address = account
.ufvk()
.and_then(|k| k.transparent())
.expect("presence of transparent key was checked above.")
.derive_internal_ivk()?
.derive_address(index)?;
(
Address::from(internal_address).to_zcash_address(params),
internal_address,
)
}
KeyScope::Ephemeral => {
let ephemeral_address = account
.ufvk()
.and_then(|k| k.transparent())
.expect("presence of transparent key was checked above.")
.derive_ephemeral_ivk()?
.derive_ephemeral_address(index)?;
(
Address::from(ephemeral_address).to_zcash_address(params),
ephemeral_address,
)
}
})
};
let gap_limit = match key_scope {
KeyScope::Zip32(zip32::Scope::External) => gap_limits.external(),
KeyScope::Zip32(zip32::Scope::Internal) => gap_limits.internal(),
@ -497,45 +560,15 @@ pub(crate) fn generate_gap_addresses<P: consensus::Parameters>(
};
if let Some(gap_start) = find_gap_start(conn, account_id, key_scope, gap_limit)? {
let range_to_store = gap_start.index()..gap_start.saturating_add(gap_limit).index();
if range_to_store.is_empty() {
return Ok(());
}
// exposed_at_height is initially NULL
let mut stmt_insert_address = conn.prepare_cached(
"INSERT INTO addresses (
account_id, diversifier_index_be, key_scope, address,
transparent_child_index, cached_transparent_receiver_address,
receiver_flags
)
VALUES (
:account_id, :diversifier_index_be, :key_scope, :address,
:transparent_child_index, :transparent_address,
:receiver_flags
)
ON CONFLICT (account_id, diversifier_index_be, key_scope) DO NOTHING",
generate_address_range(
conn,
params,
account_id,
key_scope,
request,
gap_start..gap_start.saturating_add(gap_limit),
require_key,
)?;
for raw_index in range_to_store {
let transparent_child_index = NonHardenedChildIndex::from_index(raw_index)
.expect("restricted to valid range above");
let (zcash_address, transparent_address) =
gen_addrs(key_scope, transparent_child_index)?;
let receiver_flags: ReceiverFlags = zcash_address
.clone()
.convert::<ReceiverFlags>()
.expect("address is valid");
stmt_insert_address.execute(named_params![
":account_id": account_id.0,
":diversifier_index_be": encode_diversifier_index_be(transparent_child_index.into()),
":key_scope": key_scope.encode(),
":address": zcash_address.encode(),
":transparent_child_index": raw_index,
":transparent_address": transparent_address.encode(params),
":receiver_flags": receiver_flags.bits()
])?;
}
}
Ok(())

View File

@ -7,6 +7,13 @@ and this library adheres to Rust's notion of
## [Unreleased]
## [0.2.2] - 2025-04-02
### Added
- `zcash_transparent::keys::NonHardenedChildRange`
- `zcash_transparent::keys::NonHardenedChildIter`
- `zcash_transparent::keys::NonHardenedChildIndex::const_from_index`
## [0.2.1] - 2025-03-19
### Added

View File

@ -1,7 +1,7 @@
[package]
name = "zcash_transparent"
description = "Rust implementations of the Zcash transparent protocol"
version = "0.2.1"
version = "0.2.2"
authors = [
"Jack Grigg <jack@electriccoin.co>",
"Kris Nuttycombe <kris@electriccoin.co>",

View File

@ -95,6 +95,14 @@ impl NonHardenedChildIndex {
}
}
/// Constructs a [`NonHardenedChildIndex`] from a ZIP 32 child index.
///
/// Panics: if the hardened bit is set.
pub const fn const_from_index(i: u32) -> Self {
assert!(i <= Self::MAX.0);
NonHardenedChildIndex(i)
}
/// Returns the index as a 32-bit integer.
pub const fn index(&self) -> u32 {
self.0
@ -157,6 +165,46 @@ impl From<NonHardenedChildIndex> for DiversifierIndex {
}
}
/// An end-exclusive iterator over a range of non-hardened child indexes.
pub struct NonHardenedChildIter {
next: Option<NonHardenedChildIndex>,
end: NonHardenedChildIndex,
}
impl Iterator for NonHardenedChildIter {
type Item = NonHardenedChildIndex;
fn next(&mut self) -> Option<Self::Item> {
let cur = self.next;
self.next = self
.next
.and_then(|i| i.next())
.filter(|succ| succ < &self.end);
cur
}
}
/// An end-exclusive range of non-hardened child indexes.
pub struct NonHardenedChildRange(core::ops::Range<NonHardenedChildIndex>);
impl From<core::ops::Range<NonHardenedChildIndex>> for NonHardenedChildRange {
fn from(value: core::ops::Range<NonHardenedChildIndex>) -> Self {
Self(value)
}
}
impl IntoIterator for NonHardenedChildRange {
type Item = NonHardenedChildIndex;
type IntoIter = NonHardenedChildIter;
fn into_iter(self) -> Self::IntoIter {
NonHardenedChildIter {
next: Some(self.0.start),
end: self.0.end,
}
}
}
/// A [BIP44] private key at the account path level `m/44'/<coin_type>'/<account>'`.
///
/// [BIP44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki