Merge pull request #1185 from nerdcash/nonhardenedchildindex

Declare and use NonHardenedChildIndex for transparent
This commit is contained in:
Kris Nuttycombe 2024-02-14 12:22:37 -07:00 committed by GitHub
commit 0477ec0dc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 245 additions and 96 deletions

View File

@ -52,6 +52,7 @@ and this library adheres to Rust's notion of
- `wallet::input_selection::ShieldingSelector` has been - `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent factored out from the `InputSelector` trait to separate out transparent
functionality and move it behind the `transparent-inputs` feature flag. 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::{standard, sapling}`
- `zcash_client_backend::fees::ChangeValue::new` - `zcash_client_backend::fees::ChangeValue::new`
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
@ -114,6 +115,8 @@ and this library adheres to Rust's notion of
been removed from `error::Error`. been removed from `error::Error`.
- A new variant `UnsupportedPoolType` has been added. - A new variant `UnsupportedPoolType` has been added.
- A new variant `NoSupportedReceivers` 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; - `wallet::shield_transparent_funds` no longer takes a `memo` argument;
instead, memos to be associated with the shielded outputs should be instead, memos to be associated with the shielded outputs should be
specified in the construction of the value of the `input_selector` 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 `get_unspent_transparent_outputs` have been removed; use
`data_api::InputSource` instead. `data_api::InputSource` instead.
- Added `get_account_ids`. - 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 - `wallet::{propose_shielding, shield_transparent_funds}` now takes their
`min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit `min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit
implmentations to enable zero-conf shielding. implmentations to enable zero-conf shielding.

View File

@ -14,7 +14,7 @@ use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::BlockHeight, consensus::BlockHeight,
legacy::TransparentAddress, legacy::{NonHardenedChildIndex, TransparentAddress},
memo::{Memo, MemoBytes}, memo::{Memo, MemoBytes},
transaction::{ transaction::{
components::amount::{Amount, BalanceError, NonNegativeAmount}, components::amount::{Amount, BalanceError, NonNegativeAmount},
@ -24,7 +24,7 @@ use zcash_primitives::{
}; };
use crate::{ use crate::{
address::{AddressMetadata, UnifiedAddress}, address::UnifiedAddress,
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::service::TreeState, proto::service::TreeState,
@ -56,6 +56,30 @@ pub enum NullifierQuery {
All, 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. /// Balance information for a value within a single pool in an account.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Balance { pub struct Balance {
@ -560,7 +584,7 @@ pub trait WalletRead {
fn get_transparent_receivers( fn get_transparent_receivers(
&self, &self,
account: AccountId, account: AccountId,
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error>; ) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error>;
/// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance, /// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance,
/// for each address associated with a nonzero balance. /// for each address associated with a nonzero balance.
@ -1116,7 +1140,7 @@ pub mod testing {
}; };
use crate::{ use crate::{
address::{AddressMetadata, UnifiedAddress}, address::UnifiedAddress,
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
ShieldedProtocol, ShieldedProtocol,
@ -1125,7 +1149,8 @@ pub mod testing {
use super::{ use super::{
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SentTransaction, DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SentTransaction,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, TransparentAddressMetadata, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
SAPLING_SHARD_HEIGHT,
}; };
pub struct MockWalletDb { pub struct MockWalletDb {
@ -1284,7 +1309,8 @@ pub mod testing {
fn get_transparent_receivers( fn get_transparent_receivers(
&self, &self,
_account: AccountId, _account: AccountId,
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error> { ) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error>
{
Ok(HashMap::new()) Ok(HashMap::new())
} }

View File

@ -17,7 +17,7 @@ use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::PoolType; use crate::PoolType;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; use zcash_primitives::legacy::TransparentAddress;
use crate::wallet::NoteId; use crate::wallet::NoteId;
@ -64,15 +64,18 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// Attempted to create a spend to an unsupported Unified Address receiver /// Attempted to create a spend to an unsupported Unified Address receiver
NoSupportedReceivers(Vec<u32>), NoSupportedReceivers(Vec<u32>),
/// 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 /// A note being spent does not correspond to either the internal or external
/// full viewing key for an account. /// full viewing key for an account.
NoteMismatch(NoteId), NoteMismatch(NoteId),
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
AddressNotRecognized(TransparentAddress), AddressNotRecognized(TransparentAddress),
#[cfg(feature = "transparent-inputs")]
ChildIndexOutOfRange(DiversifierIndex),
} }
impl<DE, CE, SE, FE> fmt::Display for Error<DE, CE, SE, FE> impl<DE, CE, SE, FE> fmt::Display for Error<DE, CE, SE, FE>
@ -122,20 +125,13 @@ where
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."), 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::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::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), 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")] #[cfg(feature = "transparent-inputs")]
Error::AddressNotRecognized(_) => { Error::AddressNotRecognized(_) => {
write!(f, "The specified transparent address was not recognized as belonging to the wallet.") 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
)
}
} }
} }
} }

View File

@ -5,6 +5,7 @@ use sapling::{
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
prover::{OutputProver, SpendProver}, prover::{OutputProver, SpendProver},
}; };
use zcash_keys::encoding::AddressCodec;
use zcash_primitives::{ use zcash_primitives::{
consensus::{self, NetworkUpgrade}, consensus::{self, NetworkUpgrade},
memo::MemoBytes, memo::MemoBytes,
@ -639,17 +640,17 @@ where
for utxo in proposal.transparent_inputs() { for utxo in proposal.transparent_inputs() {
utxos.push(utxo.clone()); utxos.push(utxo.clone());
let diversifier_index = known_addrs let address_metadata = known_addrs
.get(utxo.recipient_address()) .get(utxo.recipient_address())
.ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))? .ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))?
.diversifier_index(); .clone()
.ok_or_else(|| {
let child_index = u32::try_from(*diversifier_index) Error::NoSpendingKey(utxo.recipient_address().encode(params))
.map_err(|_| Error::ChildIndexOutOfRange(*diversifier_index))?; })?;
let secret_key = usk let secret_key = usk
.transparent() .transparent()
.derive_external_secret_key(child_index) .derive_external_secret_key(address_metadata.address_index())
.unwrap(); .unwrap();
builder.add_transparent_input( builder.add_transparent_input(

View File

@ -58,14 +58,14 @@ use zcash_primitives::{
}; };
use zcash_client_backend::{ use zcash_client_backend::{
address::{AddressMetadata, UnifiedAddress}, address::UnifiedAddress,
data_api::{ data_api::{
self, self,
chain::{BlockSource, CommitmentTreeRoot}, chain::{BlockSource, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange}, scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery,
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, ScannedBlock, SentTransaction, TransparentAddressMetadata, WalletCommitmentTrees,
WalletWrite, SAPLING_SHARD_HEIGHT, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
}, },
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
@ -346,7 +346,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
fn get_transparent_receivers( fn get_transparent_receivers(
&self, &self,
_account: AccountId, _account: AccountId,
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error> { ) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account); return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account);

View File

@ -112,9 +112,9 @@ use {
crate::UtxoId, crate::UtxoId,
rusqlite::Row, rusqlite::Row,
std::collections::BTreeSet, std::collections::BTreeSet,
zcash_client_backend::{address::AddressMetadata, wallet::WalletTransparentOutput}, zcash_client_backend::{data_api::TransparentAddressMetadata, wallet::WalletTransparentOutput},
zcash_primitives::{ zcash_primitives::{
legacy::{keys::IncomingViewingKey, Script, TransparentAddress}, legacy::{keys::IncomingViewingKey, NonHardenedChildIndex, Script, TransparentAddress},
transaction::components::{OutPoint, TxOut}, transaction::components::{OutPoint, TxOut},
}, },
}; };
@ -349,8 +349,8 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
conn: &rusqlite::Connection, conn: &rusqlite::Connection,
params: &P, params: &P,
account: AccountId, account: AccountId,
) -> Result<HashMap<TransparentAddress, AddressMetadata>, SqliteClientError> { ) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, SqliteClientError> {
let mut ret = HashMap::new(); let mut ret: HashMap<TransparentAddress, Option<TransparentAddressMetadata>> = HashMap::new();
// Get all UAs derived // Get all UAs derived
let mut ua_query = conn let mut ua_query = conn
@ -360,12 +360,12 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
let ua_str: String = row.get(0)?; let ua_str: String = row.get(0)?;
let di_vec: Vec<u8> = row.get(1)?; let di_vec: Vec<u8> = 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( SqliteClientError::CorruptedData(
"Diverisifier index is not an 11-byte value".to_owned(), "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) let ua = Address::decode(params, &ua_str)
.ok_or_else(|| { .ok_or_else(|| {
@ -380,16 +380,34 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
})?; })?;
if let Some(taddr) = ua.transparent() { 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( ret.insert(
*taddr, *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)? if let Some((taddr, child_index)) = get_legacy_transparent_address(params, conn, account)? {
{ ret.insert(
ret.insert(taddr, AddressMetadata::new(account, diversifier_index)); taddr,
Some(TransparentAddressMetadata::new(
Scope::External,
child_index,
)),
);
} }
Ok(ret) Ok(ret)
@ -400,7 +418,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
params: &P, params: &P,
conn: &rusqlite::Connection, conn: &rusqlite::Connection,
account: AccountId, account: AccountId,
) -> Result<Option<(TransparentAddress, DiversifierIndex)>, SqliteClientError> { ) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
// Get the UFVK for the account. // Get the UFVK for the account.
let ufvk_str: Option<String> = conn let ufvk_str: Option<String> = conn
.query_row( .query_row(
@ -418,10 +436,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
ufvk.transparent() ufvk.transparent()
.map(|tfvk| { .map(|tfvk| {
tfvk.derive_external_ivk() tfvk.derive_external_ivk()
.map(|tivk| { .map(|tivk| tivk.default_address())
let (taddr, child_index) = tivk.default_address();
(taddr, DiversifierIndex::from(child_index))
})
.map_err(SqliteClientError::HdwalletError) .map_err(SqliteClientError::HdwalletError)
}) })
.transpose() .transpose()

View File

@ -396,7 +396,9 @@ mod tests {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn migrate_from_wm2() { fn migrate_from_wm2() {
use zcash_client_backend::keys::UnifiedAddressRequest; 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; use crate::UA_TRANSPARENT;
@ -450,7 +452,7 @@ mod tests {
.and_then(|k| { .and_then(|k| {
k.derive_external_ivk() k.derive_external_ivk()
.ok() .ok()
.map(|k| k.derive_address(0).unwrap()) .map(|k| k.derive_address(NonHardenedChildIndex::ZERO).unwrap())
}) })
.map(|a| a.encode(&network)); .map(|a| a.encode(&network));

View File

@ -292,13 +292,13 @@ mod tests {
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
legacy::keys::IncomingViewingKey, legacy::{keys::IncomingViewingKey, NonHardenedChildIndex},
memo::MemoBytes, memo::MemoBytes,
transaction::{ transaction::{
builder::{BuildConfig, BuildResult, Builder}, builder::{BuildConfig, BuildResult, Builder},
components::amount::NonNegativeAmount, components::{amount::NonNegativeAmount, transparent},
fees::fixed,
}, },
transaction::{components::transparent, fees::fixed},
zip32::{AccountId, Scope}, zip32::{AccountId, Scope},
}; };
use zcash_proofs::prover::LocalTxProver; use zcash_proofs::prover::LocalTxProver;
@ -354,7 +354,9 @@ mod tests {
); );
builder builder
.add_transparent_input( .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::OutPoint::new([1; 32], 0),
transparent::TxOut { transparent::TxOut {
value: NonNegativeAmount::const_from_u64(EXTERNAL_VALUE + INTERNAL_VALUE), value: NonNegativeAmount::const_from_u64(EXTERNAL_VALUE + INTERNAL_VALUE),

View File

@ -44,3 +44,7 @@ The entries below are relative to the `zcash_client_backend` crate as of
`transparent-inputs` feature is enabled. `transparent-inputs` feature is enabled.
- `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key - `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key
argument unless the `orchard` feature is enabled. 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.

View File

@ -7,33 +7,7 @@ use zcash_address::{
unified::{self, Container, Encoding}, unified::{self, Container, Encoding},
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
}; };
use zcash_primitives::{ use zcash_primitives::{consensus, legacy::TransparentAddress};
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
}
}
/// A Unified Address. /// A Unified Address.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]

View File

@ -7,6 +7,9 @@ use zcash_primitives::{
use crate::address::UnifiedAddress; use crate::address::UnifiedAddress;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::NonHardenedChildIndex;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
std::convert::TryInto, std::convert::TryInto,
@ -68,13 +71,13 @@ pub mod sapling {
} }
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn to_transparent_child_index(j: DiversifierIndex) -> Option<u32> { fn to_transparent_child_index(j: DiversifierIndex) -> Option<NonHardenedChildIndex> {
let (low_4_bytes, rest) = j.as_bytes().split_at(4); let (low_4_bytes, rest) = j.as_bytes().split_at(4);
let transparent_j = u32::from_le_bytes(low_4_bytes.try_into().unwrap()); 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 None
} else { } else {
Some(transparent_j) NonHardenedChildIndex::from_index(transparent_j)
} }
} }
@ -388,7 +391,7 @@ impl UnifiedSpendingKey {
} }
#[cfg(all(feature = "test-dependencies", feature = "transparent-inputs"))] #[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() self.transparent()
.to_account_pubkey() .to_account_pubkey()
.derive_external_ivk() .derive_external_ivk()
@ -812,13 +815,15 @@ mod tests {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
#[test] #[test]
fn pk_to_taddr() { fn pk_to_taddr() {
use zcash_primitives::legacy::NonHardenedChildIndex;
let taddr = let taddr =
legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::ZERO) legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::ZERO)
.unwrap() .unwrap()
.to_account_pubkey() .to_account_pubkey()
.derive_external_ivk() .derive_external_ivk()
.unwrap() .unwrap()
.derive_address(0) .derive_address(NonHardenedChildIndex::ZERO)
.unwrap() .unwrap()
.encode(&MAIN_NETWORK); .encode(&MAIN_NETWORK);
assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string()); assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string());

View File

@ -7,6 +7,7 @@ and this library adheres to Rust's notion of
## [Unreleased] ## [Unreleased]
### Added ### Added
- `zcash_primitives::legacy::keys::NonHardenedChildIndex`
- Dependency on `bellman 0.14`. - Dependency on `bellman 0.14`.
- `zcash_primitives::consensus::sapling_zip212_enforcement` - `zcash_primitives::consensus::sapling_zip212_enforcement`
- `zcash_primitives::transaction`: - `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: - `zcash_client_backend` changes related to `local-consensus` feature:
- added tests that verify `zip321` supports Payment URIs with `Local(P)` - added tests that verify `zip321` supports Payment URIs with `Local(P)`
network parameters. network parameters.
- `zcash_primitives::legacy::keys::derive_external_secret_key` parameter type changed from `u32` to `NonHardenedChildIndex`.
### Removed ### Removed
- `zcash_primitives::constants`: - `zcash_primitives::constants`:

View File

@ -6,6 +6,11 @@ use std::fmt;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::ops::Shl; use std::ops::Shl;
#[cfg(feature = "transparent-inputs")]
use hdwallet::KeyIndex;
use subtle::{Choice, ConstantTimeEq};
use zcash_encoding::Vector; use zcash_encoding::Vector;
#[cfg(feature = "transparent-inputs")] #[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<Self> {
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<Self> {
// 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<KeyIndex> for NonHardenedChildIndex {
type Error = ();
fn try_from(value: KeyIndex) -> Result<Self, Self::Error> {
match value {
KeyIndex::Normal(i) => NonHardenedChildIndex::from_index(i).ok_or(()),
KeyIndex::Hardened(_) => Err(()),
}
}
}
#[cfg(feature = "transparent-inputs")]
impl From<NonHardenedChildIndex> for KeyIndex {
fn from(value: NonHardenedChildIndex) -> Self {
Self::Normal(value.index())
}
}
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::prelude::{any, prop_compose}; use proptest::prelude::{any, prop_compose};
@ -417,7 +479,9 @@ pub mod testing {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{OpCode, Script, TransparentAddress}; use super::{NonHardenedChildIndex, OpCode, Script, TransparentAddress};
use hdwallet::KeyIndex;
use subtle::ConstantTimeEq;
#[test] #[test]
fn script_opcode() { fn script_opcode() {
@ -484,4 +548,55 @@ mod tests {
); );
assert_eq!(addr.script().address(), Some(addr)); 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<T: ConstantTimeEq>(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());
}
} }

View File

@ -10,9 +10,7 @@ use zcash_spec::PrfExpand;
use crate::{consensus, zip32::AccountId}; use crate::{consensus, zip32::AccountId};
use super::TransparentAddress; use super::{NonHardenedChildIndex, TransparentAddress};
const MAX_TRANSPARENT_CHILD_INDEX: u32 = 0x7FFFFFFF;
/// A [BIP44] private key at the account path level `m/44'/<coin_type>'/<account>'`. /// A [BIP44] private key at the account path level `m/44'/<coin_type>'/<account>'`.
/// ///
@ -50,11 +48,11 @@ impl AccountPrivKey {
/// `m/44'/<coin_type>'/<account>'/0/<child_index>`. /// `m/44'/<coin_type>'/<account>'/0/<child_index>`.
pub fn derive_external_secret_key( pub fn derive_external_secret_key(
&self, &self,
child_index: u32, child_index: NonHardenedChildIndex,
) -> Result<secp256k1::SecretKey, hdwallet::error::Error> { ) -> Result<secp256k1::SecretKey, hdwallet::error::Error> {
self.0 self.0
.derive_private_key(KeyIndex::Normal(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) .map(|k| k.private_key)
} }
@ -186,30 +184,31 @@ pub trait IncomingViewingKey: private::SealedChangeLevelKey + std::marker::Sized
#[allow(deprecated)] #[allow(deprecated)]
fn derive_address( fn derive_address(
&self, &self,
child_index: u32, child_index: NonHardenedChildIndex,
) -> Result<TransparentAddress, hdwallet::error::Error> { ) -> Result<TransparentAddress, hdwallet::error::Error> {
let child_key = self let child_key = self
.extended_pubkey() .extended_pubkey()
.derive_public_key(KeyIndex::Normal(child_index))?; .derive_public_key(child_index.into())?;
Ok(pubkey_to_address(&child_key.public_key)) Ok(pubkey_to_address(&child_key.public_key))
} }
/// Searches the space of child indexes for an index that will /// Searches the space of child indexes for an index that will
/// generate a valid transparent address, and returns the resulting /// generate a valid transparent address, and returns the resulting
/// address and the index at which it was generated. /// address and the index at which it was generated.
fn default_address(&self) -> (TransparentAddress, u32) { fn default_address(&self) -> (TransparentAddress, NonHardenedChildIndex) {
let mut child_index = 0; let mut child_index = NonHardenedChildIndex::ZERO;
while child_index <= MAX_TRANSPARENT_CHILD_INDEX { loop {
match self.derive_address(child_index) { match self.derive_address(child_index) {
Ok(addr) => { Ok(addr) => {
return (addr, child_index); return (addr, child_index);
} }
Err(_) => { 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<u8> { fn serialize(&self) -> Vec<u8> {

View File

@ -950,6 +950,7 @@ mod tests {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn binding_sig_absent_if_no_shielded_spend_or_output() { fn binding_sig_absent_if_no_shielded_spend_or_output() {
use crate::consensus::NetworkUpgrade; use crate::consensus::NetworkUpgrade;
use crate::legacy::NonHardenedChildIndex;
use crate::transaction::builder::{self, TransparentBuilder}; use crate::transaction::builder::{self, TransparentBuilder};
let sapling_activation_height = TEST_NETWORK let sapling_activation_height = TEST_NETWORK
@ -984,13 +985,14 @@ mod tests {
.to_account_pubkey() .to_account_pubkey()
.derive_external_ivk() .derive_external_ivk()
.unwrap() .unwrap()
.derive_address(0) .derive_address(NonHardenedChildIndex::ZERO)
.unwrap() .unwrap()
.script(), .script(),
}; };
builder builder
.add_transparent_input( .add_transparent_input(
tsk.derive_external_secret_key(0).unwrap(), tsk.derive_external_secret_key(NonHardenedChildIndex::ZERO)
.unwrap(),
OutPoint::new([0u8; 32], 1), OutPoint::new([0u8; 32], 1),
prev_coin, prev_coin,
) )