diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index cb2c2e0b2..6b64228b8 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -52,6 +52,7 @@ and this library adheres to Rust's notion of - `wallet::input_selection::ShieldingSelector` has been factored out from the `InputSelector` trait to separate out transparent functionality and move it behind the `transparent-inputs` feature flag. + - `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`). - `zcash_client_backend::fees::{standard, sapling}` - `zcash_client_backend::fees::ChangeValue::new` - `zcash_client_backend::wallet`: @@ -114,6 +115,8 @@ and this library adheres to Rust's notion of been removed from `error::Error`. - A new variant `UnsupportedPoolType` has been added. - A new variant `NoSupportedReceivers` has been added. + - A new variant `NoSpendingKey` has been added. + - Variant `ChildIndexOutOfRange` has been removed. - `wallet::shield_transparent_funds` no longer takes a `memo` argument; instead, memos to be associated with the shielded outputs should be specified in the construction of the value of the `input_selector` @@ -161,6 +164,9 @@ and this library adheres to Rust's notion of `get_unspent_transparent_outputs` have been removed; use `data_api::InputSource` instead. - Added `get_account_ids`. + - `get_transparent_receivers` now returns + `zcash_client_backend::data_api::TransparentAddressMetadata` instead of + `zcash_keys::address::AddressMetadata`. - `wallet::{propose_shielding, shield_transparent_funds}` now takes their `min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit implmentations to enable zero-conf shielding. diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 88e30e665..83e8bca3c 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -14,7 +14,7 @@ use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; use zcash_primitives::{ block::BlockHash, consensus::BlockHeight, - legacy::TransparentAddress, + legacy::{NonHardenedChildIndex, TransparentAddress}, memo::{Memo, MemoBytes}, transaction::{ components::amount::{Amount, BalanceError, NonNegativeAmount}, @@ -24,7 +24,7 @@ use zcash_primitives::{ }; use crate::{ - address::{AddressMetadata, UnifiedAddress}, + address::UnifiedAddress, decrypt::DecryptedOutput, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, proto::service::TreeState, @@ -56,6 +56,30 @@ pub enum NullifierQuery { All, } +/// Describes the derivation of a transparent address. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransparentAddressMetadata { + scope: zip32::Scope, + address_index: NonHardenedChildIndex, +} + +impl TransparentAddressMetadata { + pub fn new(scope: zip32::Scope, address_index: NonHardenedChildIndex) -> Self { + Self { + scope, + address_index, + } + } + + pub fn scope(&self) -> zip32::Scope { + self.scope + } + + pub fn address_index(&self) -> NonHardenedChildIndex { + self.address_index + } +} + /// Balance information for a value within a single pool in an account. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Balance { @@ -560,7 +584,7 @@ pub trait WalletRead { fn get_transparent_receivers( &self, account: AccountId, - ) -> Result, Self::Error>; + ) -> Result>, Self::Error>; /// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance, /// for each address associated with a nonzero balance. @@ -1116,7 +1140,7 @@ pub mod testing { }; use crate::{ - address::{AddressMetadata, UnifiedAddress}, + address::UnifiedAddress, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, ShieldedProtocol, @@ -1125,7 +1149,8 @@ pub mod testing { use super::{ chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SentTransaction, - WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + TransparentAddressMetadata, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, + SAPLING_SHARD_HEIGHT, }; pub struct MockWalletDb { @@ -1284,7 +1309,8 @@ pub mod testing { fn get_transparent_receivers( &self, _account: AccountId, - ) -> Result, Self::Error> { + ) -> Result>, Self::Error> + { Ok(HashMap::new()) } diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs index b8dad60dc..a482bd2ef 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -17,7 +17,7 @@ use crate::data_api::wallet::input_selection::InputSelectorError; use crate::PoolType; #[cfg(feature = "transparent-inputs")] -use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; +use zcash_primitives::legacy::TransparentAddress; use crate::wallet::NoteId; @@ -64,15 +64,18 @@ pub enum Error { /// Attempted to create a spend to an unsupported Unified Address receiver NoSupportedReceivers(Vec), + /// A proposed transaction cannot be built because it requires spending an input + /// for which no spending key is available. + /// + /// The argument is the address of the note or UTXO being spent. + NoSpendingKey(String), + /// A note being spent does not correspond to either the internal or external /// full viewing key for an account. NoteMismatch(NoteId), #[cfg(feature = "transparent-inputs")] AddressNotRecognized(TransparentAddress), - - #[cfg(feature = "transparent-inputs")] - ChildIndexOutOfRange(DiversifierIndex), } impl fmt::Display for Error @@ -122,20 +125,13 @@ where Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."), Error::UnsupportedPoolType(t) => write!(f, "Attempted to send to an unsupported pool: {}", t), Error::NoSupportedReceivers(t) => write!(f, "Unified address contained only unsupported receiver types: {:?}", &t[..]), + Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr), Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n), #[cfg(feature = "transparent-inputs")] Error::AddressNotRecognized(_) => { write!(f, "The specified transparent address was not recognized as belonging to the wallet.") } - #[cfg(feature = "transparent-inputs")] - Error::ChildIndexOutOfRange(i) => { - write!( - f, - "The diversifier index {:?} is out of range for transparent addresses.", - i - ) - } } } } diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 04f301830..f342e94dc 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -5,6 +5,7 @@ use sapling::{ note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, prover::{OutputProver, SpendProver}, }; +use zcash_keys::encoding::AddressCodec; use zcash_primitives::{ consensus::{self, NetworkUpgrade}, memo::MemoBytes, @@ -639,17 +640,17 @@ where for utxo in proposal.transparent_inputs() { utxos.push(utxo.clone()); - let diversifier_index = known_addrs + let address_metadata = known_addrs .get(utxo.recipient_address()) .ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))? - .diversifier_index(); - - let child_index = u32::try_from(*diversifier_index) - .map_err(|_| Error::ChildIndexOutOfRange(*diversifier_index))?; + .clone() + .ok_or_else(|| { + Error::NoSpendingKey(utxo.recipient_address().encode(params)) + })?; let secret_key = usk .transparent() - .derive_external_secret_key(child_index) + .derive_external_secret_key(address_metadata.address_index()) .unwrap(); builder.add_transparent_input( diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index ff8502b0a..5524c5b81 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -58,14 +58,14 @@ use zcash_primitives::{ }; use zcash_client_backend::{ - address::{AddressMetadata, UnifiedAddress}, + address::UnifiedAddress, data_api::{ self, chain::{BlockSource, CommitmentTreeRoot}, scanning::{ScanPriority, ScanRange}, AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, - ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, - WalletWrite, SAPLING_SHARD_HEIGHT, + ScannedBlock, SentTransaction, TransparentAddressMetadata, WalletCommitmentTrees, + WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, }, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, proto::compact_formats::CompactBlock, @@ -346,7 +346,7 @@ impl, P: consensus::Parameters> WalletRead for W fn get_transparent_receivers( &self, _account: AccountId, - ) -> Result, Self::Error> { + ) -> Result>, Self::Error> { #[cfg(feature = "transparent-inputs")] return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account); diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 86fdb51d9..cbc561dcf 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -112,9 +112,9 @@ use { crate::UtxoId, rusqlite::Row, std::collections::BTreeSet, - zcash_client_backend::{address::AddressMetadata, wallet::WalletTransparentOutput}, + zcash_client_backend::{data_api::TransparentAddressMetadata, wallet::WalletTransparentOutput}, zcash_primitives::{ - legacy::{keys::IncomingViewingKey, Script, TransparentAddress}, + legacy::{keys::IncomingViewingKey, NonHardenedChildIndex, Script, TransparentAddress}, transaction::components::{OutPoint, TxOut}, }, }; @@ -349,8 +349,8 @@ pub(crate) fn get_transparent_receivers( conn: &rusqlite::Connection, params: &P, account: AccountId, -) -> Result, SqliteClientError> { - let mut ret = HashMap::new(); +) -> Result>, SqliteClientError> { + let mut ret: HashMap> = HashMap::new(); // Get all UAs derived let mut ua_query = conn @@ -360,12 +360,12 @@ pub(crate) fn get_transparent_receivers( while let Some(row) = rows.next()? { let ua_str: String = row.get(0)?; let di_vec: Vec = row.get(1)?; - let mut di_be: [u8; 11] = di_vec.try_into().map_err(|_| { + let mut di: [u8; 11] = di_vec.try_into().map_err(|_| { SqliteClientError::CorruptedData( "Diverisifier index is not an 11-byte value".to_owned(), ) })?; - di_be.reverse(); + di.reverse(); // BE -> LE conversion let ua = Address::decode(params, &ua_str) .ok_or_else(|| { @@ -380,16 +380,34 @@ pub(crate) fn get_transparent_receivers( })?; if let Some(taddr) = ua.transparent() { + let index = NonHardenedChildIndex::from_index( + DiversifierIndex::from(di).try_into().map_err(|_| { + SqliteClientError::CorruptedData( + "Unable to get diversifier for transparent address.".to_string(), + ) + })?, + ) + .ok_or_else(|| { + SqliteClientError::CorruptedData( + "Unexpected hardened index for transparent address.".to_string(), + ) + })?; + ret.insert( *taddr, - AddressMetadata::new(account, DiversifierIndex::from(di_be)), + Some(TransparentAddressMetadata::new(Scope::External, index)), ); } } - if let Some((taddr, diversifier_index)) = get_legacy_transparent_address(params, conn, account)? - { - ret.insert(taddr, AddressMetadata::new(account, diversifier_index)); + if let Some((taddr, child_index)) = get_legacy_transparent_address(params, conn, account)? { + ret.insert( + taddr, + Some(TransparentAddressMetadata::new( + Scope::External, + child_index, + )), + ); } Ok(ret) @@ -400,7 +418,7 @@ pub(crate) fn get_legacy_transparent_address( params: &P, conn: &rusqlite::Connection, account: AccountId, -) -> Result, SqliteClientError> { +) -> Result, SqliteClientError> { // Get the UFVK for the account. let ufvk_str: Option = conn .query_row( @@ -418,10 +436,7 @@ pub(crate) fn get_legacy_transparent_address( ufvk.transparent() .map(|tfvk| { tfvk.derive_external_ivk() - .map(|tivk| { - let (taddr, child_index) = tivk.default_address(); - (taddr, DiversifierIndex::from(child_index)) - }) + .map(|tivk| tivk.default_address()) .map_err(SqliteClientError::HdwalletError) }) .transpose() diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs index 4e48e9ce0..3fb5903f1 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -396,7 +396,9 @@ mod tests { #[cfg(feature = "transparent-inputs")] fn migrate_from_wm2() { use zcash_client_backend::keys::UnifiedAddressRequest; - use zcash_primitives::transaction::components::amount::NonNegativeAmount; + use zcash_primitives::{ + legacy::NonHardenedChildIndex, transaction::components::amount::NonNegativeAmount, + }; use crate::UA_TRANSPARENT; @@ -450,7 +452,7 @@ mod tests { .and_then(|k| { k.derive_external_ivk() .ok() - .map(|k| k.derive_address(0).unwrap()) + .map(|k| k.derive_address(NonHardenedChildIndex::ZERO).unwrap()) }) .map(|a| a.encode(&network)); diff --git a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs index 3315a3f65..7344329a4 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -292,13 +292,13 @@ mod tests { use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, - legacy::keys::IncomingViewingKey, + legacy::{keys::IncomingViewingKey, NonHardenedChildIndex}, memo::MemoBytes, transaction::{ builder::{BuildConfig, BuildResult, Builder}, - components::amount::NonNegativeAmount, + components::{amount::NonNegativeAmount, transparent}, + fees::fixed, }, - transaction::{components::transparent, fees::fixed}, zip32::{AccountId, Scope}, }; use zcash_proofs::prover::LocalTxProver; @@ -354,7 +354,9 @@ mod tests { ); builder .add_transparent_input( - usk0.transparent().derive_external_secret_key(0).unwrap(), + usk0.transparent() + .derive_external_secret_key(NonHardenedChildIndex::ZERO) + .unwrap(), transparent::OutPoint::new([1; 32], 0), transparent::TxOut { value: NonNegativeAmount::const_from_u64(EXTERNAL_VALUE + INTERNAL_VALUE), diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index 9e8457074..752a27f29 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -44,3 +44,7 @@ The entries below are relative to the `zcash_client_backend` crate as of `transparent-inputs` feature is enabled. - `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key argument unless the `orchard` feature is enabled. + +### Removed +- `zcash_keys::address::AddressMetadata` has been moved to + `zcash_client_backend::data_api::TransparentAddressMetadata` and fields changed. diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 54c0358e6..dcb3b8a61 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -7,33 +7,7 @@ use zcash_address::{ unified::{self, Container, Encoding}, ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, }; -use zcash_primitives::{ - consensus, - legacy::TransparentAddress, - zip32::{AccountId, DiversifierIndex}, -}; - -pub struct AddressMetadata { - account: AccountId, - diversifier_index: DiversifierIndex, -} - -impl AddressMetadata { - pub fn new(account: AccountId, diversifier_index: DiversifierIndex) -> Self { - Self { - account, - diversifier_index, - } - } - - pub fn account(&self) -> AccountId { - self.account - } - - pub fn diversifier_index(&self) -> &DiversifierIndex { - &self.diversifier_index - } -} +use zcash_primitives::{consensus, legacy::TransparentAddress}; /// A Unified Address. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 1693a780d..1fc1b9e73 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -7,6 +7,9 @@ use zcash_primitives::{ use crate::address::UnifiedAddress; +#[cfg(feature = "transparent-inputs")] +use zcash_primitives::legacy::NonHardenedChildIndex; + #[cfg(feature = "transparent-inputs")] use { std::convert::TryInto, @@ -68,13 +71,13 @@ pub mod sapling { } #[cfg(feature = "transparent-inputs")] -fn to_transparent_child_index(j: DiversifierIndex) -> Option { +fn to_transparent_child_index(j: DiversifierIndex) -> Option { let (low_4_bytes, rest) = j.as_bytes().split_at(4); let transparent_j = u32::from_le_bytes(low_4_bytes.try_into().unwrap()); - if transparent_j > (0x7FFFFFFF) || rest.iter().any(|b| b != &0) { + if rest.iter().any(|b| b != &0) { None } else { - Some(transparent_j) + NonHardenedChildIndex::from_index(transparent_j) } } @@ -388,7 +391,7 @@ impl UnifiedSpendingKey { } #[cfg(all(feature = "test-dependencies", feature = "transparent-inputs"))] - pub fn default_transparent_address(&self) -> (TransparentAddress, u32) { + pub fn default_transparent_address(&self) -> (TransparentAddress, NonHardenedChildIndex) { self.transparent() .to_account_pubkey() .derive_external_ivk() @@ -812,13 +815,15 @@ mod tests { #[cfg(feature = "transparent-inputs")] #[test] fn pk_to_taddr() { + use zcash_primitives::legacy::NonHardenedChildIndex; + let taddr = legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::ZERO) .unwrap() .to_account_pubkey() .derive_external_ivk() .unwrap() - .derive_address(0) + .derive_address(NonHardenedChildIndex::ZERO) .unwrap() .encode(&MAIN_NETWORK); assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string()); diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index 8a858a6e8..875259ca2 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -7,6 +7,7 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Added +- `zcash_primitives::legacy::keys::NonHardenedChildIndex` - Dependency on `bellman 0.14`. - `zcash_primitives::consensus::sapling_zip212_enforcement` - `zcash_primitives::transaction`: @@ -126,6 +127,7 @@ and this library adheres to Rust's notion of - `zcash_client_backend` changes related to `local-consensus` feature: - added tests that verify `zip321` supports Payment URIs with `Local(P)` network parameters. +- `zcash_primitives::legacy::keys::derive_external_secret_key` parameter type changed from `u32` to `NonHardenedChildIndex`. ### Removed - `zcash_primitives::constants`: diff --git a/zcash_primitives/src/legacy.rs b/zcash_primitives/src/legacy.rs index 398d9ab97..5ddf0c5a3 100644 --- a/zcash_primitives/src/legacy.rs +++ b/zcash_primitives/src/legacy.rs @@ -6,6 +6,11 @@ use std::fmt; use std::io::{self, Read, Write}; use std::ops::Shl; +#[cfg(feature = "transparent-inputs")] +use hdwallet::KeyIndex; + +use subtle::{Choice, ConstantTimeEq}; + use zcash_encoding::Vector; #[cfg(feature = "transparent-inputs")] @@ -402,6 +407,63 @@ impl TransparentAddress { } } +/// A child index for a derived transparent address. +/// +/// Only NON-hardened derivation is supported. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct NonHardenedChildIndex(u32); + +impl ConstantTimeEq for NonHardenedChildIndex { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl NonHardenedChildIndex { + pub const ZERO: NonHardenedChildIndex = NonHardenedChildIndex(0); + + /// Parses the given ZIP 32 child index. + /// + /// Returns `None` if the hardened bit is set. + pub fn from_index(i: u32) -> Option { + if i < (1 << 31) { + Some(NonHardenedChildIndex(i)) + } else { + None + } + } + + /// Returns the index as a 32-bit integer. + pub fn index(&self) -> u32 { + self.0 + } + + pub fn next(&self) -> Option { + // overflow cannot happen because self.0 is 31 bits, and the next index is at most 32 bits + // which in that case would lead from_index to return None. + Self::from_index(self.0 + 1) + } +} + +#[cfg(feature = "transparent-inputs")] +impl TryFrom for NonHardenedChildIndex { + type Error = (); + + fn try_from(value: KeyIndex) -> Result { + match value { + KeyIndex::Normal(i) => NonHardenedChildIndex::from_index(i).ok_or(()), + KeyIndex::Hardened(_) => Err(()), + } + } +} + +#[cfg(feature = "transparent-inputs")] +impl From for KeyIndex { + fn from(value: NonHardenedChildIndex) -> Self { + Self::Normal(value.index()) + } +} + #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::prelude::{any, prop_compose}; @@ -417,7 +479,9 @@ pub mod testing { #[cfg(test)] mod tests { - use super::{OpCode, Script, TransparentAddress}; + use super::{NonHardenedChildIndex, OpCode, Script, TransparentAddress}; + use hdwallet::KeyIndex; + use subtle::ConstantTimeEq; #[test] fn script_opcode() { @@ -484,4 +548,55 @@ mod tests { ); assert_eq!(addr.script().address(), Some(addr)); } + + #[test] + fn nonhardened_indexes_accepted() { + assert_eq!(0, NonHardenedChildIndex::from_index(0).unwrap().index()); + assert_eq!( + 0x7fffffff, + NonHardenedChildIndex::from_index(0x7fffffff) + .unwrap() + .index() + ); + } + + #[test] + fn hardened_indexes_rejected() { + assert!(NonHardenedChildIndex::from_index(0x80000000).is_none()); + assert!(NonHardenedChildIndex::from_index(0xffffffff).is_none()); + } + + #[test] + fn nonhardened_index_next() { + assert_eq!(1, NonHardenedChildIndex::ZERO.next().unwrap().index()); + assert!(NonHardenedChildIndex::from_index(0x7fffffff) + .unwrap() + .next() + .is_none()); + } + + #[test] + fn nonhardened_index_ct_eq() { + assert!(check( + NonHardenedChildIndex::ZERO, + NonHardenedChildIndex::ZERO + )); + assert!(!check( + NonHardenedChildIndex::ZERO, + NonHardenedChildIndex::ZERO.next().unwrap() + )); + + fn check(v1: T, v2: T) -> bool { + v1.ct_eq(&v2).into() + } + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn nonhardened_index_tryfrom_keyindex() { + let nh: NonHardenedChildIndex = KeyIndex::Normal(0).try_into().unwrap(); + assert_eq!(nh.index(), 0); + + assert!(NonHardenedChildIndex::try_from(KeyIndex::Hardened(0)).is_err()); + } } diff --git a/zcash_primitives/src/legacy/keys.rs b/zcash_primitives/src/legacy/keys.rs index 7bbc2f106..85e53f30e 100644 --- a/zcash_primitives/src/legacy/keys.rs +++ b/zcash_primitives/src/legacy/keys.rs @@ -10,9 +10,7 @@ use zcash_spec::PrfExpand; use crate::{consensus, zip32::AccountId}; -use super::TransparentAddress; - -const MAX_TRANSPARENT_CHILD_INDEX: u32 = 0x7FFFFFFF; +use super::{NonHardenedChildIndex, TransparentAddress}; /// A [BIP44] private key at the account path level `m/44'/'/'`. /// @@ -50,11 +48,11 @@ impl AccountPrivKey { /// `m/44'/'/'/0/`. pub fn derive_external_secret_key( &self, - child_index: u32, + child_index: NonHardenedChildIndex, ) -> Result { self.0 .derive_private_key(KeyIndex::Normal(0))? - .derive_private_key(KeyIndex::Normal(child_index)) + .derive_private_key(child_index.into()) .map(|k| k.private_key) } @@ -186,30 +184,31 @@ pub trait IncomingViewingKey: private::SealedChangeLevelKey + std::marker::Sized #[allow(deprecated)] fn derive_address( &self, - child_index: u32, + child_index: NonHardenedChildIndex, ) -> Result { let child_key = self .extended_pubkey() - .derive_public_key(KeyIndex::Normal(child_index))?; + .derive_public_key(child_index.into())?; Ok(pubkey_to_address(&child_key.public_key)) } /// Searches the space of child indexes for an index that will /// generate a valid transparent address, and returns the resulting /// address and the index at which it was generated. - fn default_address(&self) -> (TransparentAddress, u32) { - let mut child_index = 0; - while child_index <= MAX_TRANSPARENT_CHILD_INDEX { + fn default_address(&self) -> (TransparentAddress, NonHardenedChildIndex) { + let mut child_index = NonHardenedChildIndex::ZERO; + loop { match self.derive_address(child_index) { Ok(addr) => { return (addr, child_index); } Err(_) => { - child_index += 1; + child_index = child_index.next().unwrap_or_else(|| { + panic!("Exhausted child index space attempting to find a default address."); + }); } } } - panic!("Exhausted child index space attempting to find a default address."); } fn serialize(&self) -> Vec { diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index 20f749ffd..cfe02aea1 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -950,6 +950,7 @@ mod tests { #[cfg(feature = "transparent-inputs")] fn binding_sig_absent_if_no_shielded_spend_or_output() { use crate::consensus::NetworkUpgrade; + use crate::legacy::NonHardenedChildIndex; use crate::transaction::builder::{self, TransparentBuilder}; let sapling_activation_height = TEST_NETWORK @@ -984,13 +985,14 @@ mod tests { .to_account_pubkey() .derive_external_ivk() .unwrap() - .derive_address(0) + .derive_address(NonHardenedChildIndex::ZERO) .unwrap() .script(), }; builder .add_transparent_input( - tsk.derive_external_secret_key(0).unwrap(), + tsk.derive_external_secret_key(NonHardenedChildIndex::ZERO) + .unwrap(), OutPoint::new([0u8; 32], 1), prev_coin, )