From 61bb18d97fa967aaf459455f2794fac620bcae61 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Nov 2023 02:08:19 +0000 Subject: [PATCH] zcash_primitives: Refactor `zip32::ChildIndex` to be an opaque struct --- zcash_client_backend/src/keys.rs | 6 +-- zcash_primitives/CHANGELOG.md | 8 +++ zcash_primitives/src/zip32.rs | 35 +++++++----- zcash_primitives/src/zip32/sapling.rs | 77 ++++++++++++++++++--------- 4 files changed, 85 insertions(+), 41 deletions(-) diff --git a/zcash_client_backend/src/keys.rs b/zcash_client_backend/src/keys.rs index a850b618d..2a96559ab 100644 --- a/zcash_client_backend/src/keys.rs +++ b/zcash_client_backend/src/keys.rs @@ -62,9 +62,9 @@ pub mod sapling { ExtendedSpendingKey::from_path( &ExtendedSpendingKey::master(seed), &[ - ChildIndex::Hardened(32), - ChildIndex::Hardened(coin_type), - ChildIndex::Hardened(account.into()), + ChildIndex::hardened(32), + ChildIndex::hardened(coin_type), + account.into(), ], ) } diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index 28fd6f568..4022891e6 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -71,6 +71,10 @@ and this library adheres to Rust's notion of - `GRACE_ACTIONS` - `P2PKH_STANDARD_INPUT_SIZE` - `P2PKH_STANDARD_OUTPUT_SIZE` +- `zcash_primitives::zip32`: + - `ChildIndex::hardened` + - `ChildIndex::index` + - `impl From for ChildIndex` - Test helpers, behind the `test-dependencies` feature flag: - `zcash_primitives::sapling::prover::mock::{MockSpendProver, MockOutputProver}` - Additions related to `zcash_primitive::components::amount::Amount` @@ -155,6 +159,9 @@ and this library adheres to Rust's notion of - `fees::fixed::FeeRule::fixed_fee` - `fees::zip317::FeeRule::marginal_fee` - `sighash::TransparentAuthorizingContext::input_amounts` +- `zcash_primitives::zip32`: + - `ChildIndex` has been changed from an enum to an opaque struct, and no + longer supports non-hardened indices. ### Removed - `zcash_primitives::constants`: @@ -189,6 +196,7 @@ and this library adheres to Rust's notion of `Bundle::>::apply_signatures` instead). - `impl From for u64` - `zcash_primitives::zip32`: + - `ChildIndex::Hardened` (use `ChildIndex::hardened` instead). - `ChildIndex::NonHardened` - `sapling::ExtendedFullViewingKey::derive_child` diff --git a/zcash_primitives/src/zip32.rs b/zcash_primitives/src/zip32.rs index 9d132cb77..1e7a91560 100644 --- a/zcash_primitives/src/zip32.rs +++ b/zcash_primitives/src/zip32.rs @@ -35,6 +35,13 @@ impl From for u32 { } } +impl From for ChildIndex { + fn from(id: AccountId) -> Self { + // Account IDs are always hardened in derivation paths. + ChildIndex::hardened(id.0) + } +} + impl ConditionallySelectable for AccountId { fn conditional_select(a0: &Self, a1: &Self, c: Choice) -> Self { AccountId(u32::conditional_select(&a0.0, &a1.0, c)) @@ -43,30 +50,34 @@ impl ConditionallySelectable for AccountId { // ZIP 32 structures -/// A child index for a derived key +/// A child index for a derived key. +/// +/// Only hardened derivation is supported. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ChildIndex { - Hardened(u32), // Hardened(n) == n + (1 << 31) == n' in path notation -} +pub struct ChildIndex(u32); impl ChildIndex { pub fn from_index(i: u32) -> Option { if i >= (1 << 31) { - Some(ChildIndex::Hardened(i - (1 << 31))) + Some(ChildIndex(i)) } else { None } } - fn master() -> Self { - // TODO: This is invalid; fixed in next commit. - ChildIndex::Hardened(0) + /// Constructs a hardened `ChildIndex` from the given value. + /// + /// # Panics + /// + /// Panics if `value >= (1 << 31)`. + pub const fn hardened(value: u32) -> Self { + assert!(value < (1 << 31)); + Self(value + (1 << 31)) } - fn value(&self) -> u32 { - match *self { - ChildIndex::Hardened(i) => i + (1 << 31), - } + /// Returns the index as a 32-bit integer, including the hardened bit. + pub fn index(&self) -> u32 { + self.0 } } diff --git a/zcash_primitives/src/zip32/sapling.rs b/zcash_primitives/src/zip32/sapling.rs index e8168365c..ace85a64b 100644 --- a/zcash_primitives/src/zip32/sapling.rs +++ b/zcash_primitives/src/zip32/sapling.rs @@ -231,12 +231,39 @@ impl DiversifierKey { } } +/// The derivation index associated with a key. +/// +/// Master keys are never derived via the ZIP 32 child derivation process, but they have +/// an index in their encoding. This type allows the encoding to be represented, while +/// also enabling the derivation methods to only accept [`ChildIndex`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum KeyIndex { + Master, + Child(ChildIndex), +} + +impl KeyIndex { + fn new(depth: u8, i: u32) -> Option { + match i { + 0 if depth == 0 => Some(KeyIndex::Master), + _ => ChildIndex::from_index(i).map(KeyIndex::Child), + } + } + + fn index(&self) -> u32 { + match self { + KeyIndex::Master => 0, + KeyIndex::Child(i) => i.index(), + } + } +} + /// A Sapling extended spending key #[derive(Clone)] pub struct ExtendedSpendingKey { depth: u8, parent_fvk_tag: FvkTag, - child_index: ChildIndex, + child_index: KeyIndex, chain_code: ChainCode, pub expsk: ExpandedSpendingKey, dk: DiversifierKey, @@ -279,7 +306,7 @@ impl ExtendedSpendingKey { ExtendedSpendingKey { depth: 0, parent_fvk_tag: FvkTag::master(), - child_index: ChildIndex::master(), + child_index: KeyIndex::Master, chain_code: ChainCode(c_m), expsk: ExpandedSpendingKey::from_spending_key(sk_m), dk: DiversifierKey::master(sk_m), @@ -303,7 +330,7 @@ impl ExtendedSpendingKey { let mut ci_bytes = [0u8; 4]; ci_bytes[..].copy_from_slice(&b[5..9]); - let child_index = ChildIndex::from_index(u32::from_le_bytes(ci_bytes)) + let child_index = KeyIndex::new(depth, u32::from_le_bytes(ci_bytes)) .ok_or(DecodingError::UnsupportedChildIndex)?; let mut chain_code = ChainCode([0u8; 32]); @@ -331,7 +358,7 @@ impl ExtendedSpendingKey { let mut tag = [0; 4]; reader.read_exact(&mut tag)?; let child_index = reader.read_u32::().and_then(|i| { - ChildIndex::from_index(i).ok_or_else(|| { + KeyIndex::new(depth, i).ok_or_else(|| { io::Error::new( io::ErrorKind::Unsupported, "Non-hardened keys are not supported", @@ -360,7 +387,7 @@ impl ExtendedSpendingKey { let mut result = [0u8; 169]; result[0] = self.depth; result[1..5].copy_from_slice(&self.parent_fvk_tag.as_bytes()[..]); - result[5..9].copy_from_slice(&self.child_index.value().to_le_bytes()[..]); + result[5..9].copy_from_slice(&self.child_index.index().to_le_bytes()[..]); result[9..41].copy_from_slice(&self.chain_code.as_bytes()[..]); result[41..137].copy_from_slice(&self.expsk.to_bytes()[..]); result[137..169].copy_from_slice(&self.dk.as_bytes()[..]); @@ -385,15 +412,13 @@ impl ExtendedSpendingKey { #[must_use] pub fn derive_child(&self, i: ChildIndex) -> Self { let fvk = FullViewingKey::from_expanded_spending_key(&self.expsk); - let tmp = match i { - ChildIndex::Hardened(i) => { - let mut le_i = [0; 4]; - LittleEndian::write_u32(&mut le_i, i + (1 << 31)); - prf_expand_vec( - &self.chain_code.0, - &[&[0x11], &self.expsk.to_bytes(), &self.dk.0, &le_i], - ) - } + let tmp = { + let mut le_i = [0; 4]; + LittleEndian::write_u32(&mut le_i, i.index()); + prf_expand_vec( + &self.chain_code.0, + &[&[0x11], &self.expsk.to_bytes(), &self.dk.0, &le_i], + ) }; let i_l = &tmp.as_bytes()[..32]; let mut c_i = [0u8; 32]; @@ -402,7 +427,7 @@ impl ExtendedSpendingKey { ExtendedSpendingKey { depth: self.depth + 1, parent_fvk_tag: FvkFingerprint::from(&fvk).tag(), - child_index: i, + child_index: KeyIndex::Child(i), chain_code: ChainCode(c_i), expsk: { let mut ask = jubjub::Fr::from_bytes_wide(prf_expand(i_l, &[0x13]).as_array()); @@ -483,7 +508,7 @@ impl ExtendedSpendingKey { pub struct ExtendedFullViewingKey { depth: u8, parent_fvk_tag: FvkTag, - child_index: ChildIndex, + child_index: KeyIndex, chain_code: ChainCode, pub fvk: FullViewingKey, pub(crate) dk: DiversifierKey, @@ -518,7 +543,7 @@ impl ExtendedFullViewingKey { let mut tag = [0; 4]; reader.read_exact(&mut tag)?; let child_index = reader.read_u32::().and_then(|i| { - ChildIndex::from_index(i).ok_or_else(|| { + KeyIndex::new(depth, i).ok_or_else(|| { io::Error::new( io::ErrorKind::Unsupported, "Non-hardened keys are not supported", @@ -544,7 +569,7 @@ impl ExtendedFullViewingKey { pub fn write(&self, mut writer: W) -> io::Result<()> { writer.write_u8(self.depth)?; writer.write_all(&self.parent_fvk_tag.0)?; - writer.write_u32::(self.child_index.value())?; + writer.write_u32::(self.child_index.index())?; writer.write_all(&self.chain_code.0)?; writer.write_all(&self.fvk.to_bytes())?; writer.write_all(&self.dk.0)?; @@ -787,7 +812,7 @@ mod tests { let seed = [0; 32]; let xsk_m = ExtendedSpendingKey::master(&seed); - let i_5h = ChildIndex::Hardened(5); + let i_5h = ChildIndex::hardened(5); let _ = xsk_m.derive_child(i_5h); } @@ -796,17 +821,17 @@ mod tests { let seed = [0; 32]; let xsk_m = ExtendedSpendingKey::master(&seed); - let xsk_5h = xsk_m.derive_child(ChildIndex::Hardened(5)); + let xsk_5h = xsk_m.derive_child(ChildIndex::hardened(5)); assert_eq!( - ExtendedSpendingKey::from_path(&xsk_m, &[ChildIndex::Hardened(5)]), + ExtendedSpendingKey::from_path(&xsk_m, &[ChildIndex::hardened(5)]), xsk_5h ); - let xsk_5h_7 = xsk_5h.derive_child(ChildIndex::Hardened(7)); + let xsk_5h_7 = xsk_5h.derive_child(ChildIndex::hardened(7)); assert_eq!( ExtendedSpendingKey::from_path( &xsk_m, - &[ChildIndex::Hardened(5), ChildIndex::Hardened(7)] + &[ChildIndex::hardened(5), ChildIndex::hardened(7)] ), xsk_5h_7 ); @@ -1567,9 +1592,9 @@ mod tests { 24, 25, 26, 27, 28, 29, 30, 31, ]; - let i1h = ChildIndex::Hardened(1); - let i2h = ChildIndex::Hardened(2); - let i3h = ChildIndex::Hardened(3); + let i1h = ChildIndex::hardened(1); + let i2h = ChildIndex::hardened(2); + let i3h = ChildIndex::hardened(3); let m = ExtendedSpendingKey::master(&seed); let m_1h = m.derive_child(i1h);