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:
commit
2b89d80f12
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>"
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue