Skip to main content

zcash_address/kind/unified/
ivk.rs

1use alloc::vec::Vec;
2use core::{
3    convert::{TryFrom, TryInto},
4    fmt,
5};
6use zcash_protocol::constants;
7
8use super::{
9    private::{SealedContainer, SealedItem},
10    Container, Encoding, ParseError, Typecode,
11};
12
13/// The set of known IVKs for Unified IVKs.
14#[derive(Clone, PartialEq, Eq, Hash)]
15pub enum Ivk {
16    /// The raw encoding of an Orchard Incoming Viewing Key.
17    ///
18    /// `(dk, ivk)` each 32 bytes.
19    Orchard([u8; 64]),
20
21    /// Data contained within the Sapling component of a Unified Incoming Viewing Key.
22    ///
23    /// In order to ensure that Unified Addresses can always be derived from UIVKs, we
24    /// store more data here than was specified to be part of a Sapling IVK. Specifically,
25    /// we store the same data here as we do for Orchard.
26    ///
27    /// `(dk, ivk)` each 32 bytes.
28    Sapling([u8; 64]),
29
30    /// A pruned version of the extended public key for the BIP 44 account corresponding to the
31    /// transparent address subtree from which transparent addresses are derived,
32    /// at the external `change` BIP 44 path, i.e. `m/44'/133'/<account_id>'/0`. This
33    /// includes just the chain code (32 bytes) and the compressed public key (33 bytes), and excludes
34    /// the depth of in the derivation tree, the parent key fingerprint, and the child key
35    /// number (which would reveal the wallet account number for which this UFVK was generated).
36    ///
37    /// Transparent addresses don't have "viewing keys" - the addresses themselves serve
38    /// that purpose. However, we want the ability to derive diversified Unified Addresses
39    /// from Unified Viewing Keys, and to not break the unlinkability property when they
40    /// include transparent receivers. To achieve this, we treat the last hardened node in
41    /// the BIP 44 derivation path as the "transparent viewing key"; all addresses derived
42    /// from this node use non-hardened derivation, and can thus be derived just from this
43    /// pruned extended public key.
44    P2pkh([u8; 65]),
45
46    Unknown {
47        typecode: u32,
48        data: Vec<u8>,
49    },
50}
51
52impl fmt::Debug for Ivk {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Ivk::Orchard(_) => f.debug_tuple("Ivk::Orchard").field(&"...").finish(),
56            Ivk::Sapling(_) => f.debug_tuple("Ivk::Sapling").field(&"...").finish(),
57            Ivk::P2pkh(_) => f.debug_tuple("Ivk::P2pkh").field(&"...").finish(),
58            Ivk::Unknown { typecode, .. } => f
59                .debug_struct("Ivk::Unknown")
60                .field("typecode", typecode)
61                .field("data", &"...")
62                .finish(),
63        }
64    }
65}
66
67impl TryFrom<(u32, &[u8])> for Ivk {
68    type Error = ParseError;
69
70    fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
71        let data = data.to_vec();
72        match typecode.try_into()? {
73            Typecode::P2pkh => data.try_into().map(Ivk::P2pkh),
74            Typecode::P2sh => Err(data),
75            Typecode::Sapling => data.try_into().map(Ivk::Sapling),
76            Typecode::Orchard => data.try_into().map(Ivk::Orchard),
77            Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }),
78        }
79        .map_err(|e| {
80            ParseError::InvalidEncoding(format!("Invalid ivk for typecode {}: {:?}", typecode, e))
81        })
82    }
83}
84
85impl SealedItem for Ivk {
86    fn typecode(&self) -> Typecode {
87        match self {
88            Ivk::P2pkh(_) => Typecode::P2pkh,
89            Ivk::Sapling(_) => Typecode::Sapling,
90            Ivk::Orchard(_) => Typecode::Orchard,
91            Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
92        }
93    }
94
95    fn data(&self) -> &[u8] {
96        match self {
97            Ivk::P2pkh(data) => data,
98            Ivk::Sapling(data) => data,
99            Ivk::Orchard(data) => data,
100            Ivk::Unknown { data, .. } => data,
101        }
102    }
103}
104
105/// A Unified Incoming Viewing Key.
106///
107/// # Examples
108///
109/// ```
110/// use zcash_address::unified::{self, Container, Encoding};
111///
112/// # #[cfg(not(feature = "std"))]
113/// # fn main() {}
114/// # #[cfg(feature = "std")]
115/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
116/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
117/// let example_uivk: &str = uivk_from_user();
118///
119/// let (network, uivk) = unified::Uivk::decode(example_uivk)?;
120///
121/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in
122/// // preference order (the order in which wallets should prefer to use their
123/// // corresponding address receivers):
124/// let ivks: Vec<unified::Ivk> = uivk.items();
125///
126/// // And we can create the UIVK from a list of IVKs:
127/// let new_uivk = unified::Uivk::try_from_items(ivks)?;
128/// assert_eq!(new_uivk, uivk);
129/// # Ok(())
130/// # }
131/// ```
132#[derive(Clone, Debug, PartialEq, Eq, Hash)]
133pub struct Uivk(pub(crate) Vec<Ivk>);
134
135impl Container for Uivk {
136    type Item = Ivk;
137
138    /// Returns the IVKs contained within this UIVK, in the order they were
139    /// parsed from the string encoding.
140    ///
141    /// This API is for advanced usage; in most cases you should use `Uivk::items`.
142    fn items_as_parsed(&self) -> &[Ivk] {
143        &self.0
144    }
145}
146
147impl Encoding for Uivk {}
148
149impl SealedContainer for Uivk {
150    /// The HRP for a Bech32m-encoded mainnet Unified IVK.
151    ///
152    /// Defined in [ZIP 316][zip-0316].
153    ///
154    /// [zip-0316]: https://zips.z.cash/zip-0316
155    const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_IVK;
156
157    /// The HRP for a Bech32m-encoded testnet Unified IVK.
158    ///
159    /// Defined in [ZIP 316][zip-0316].
160    ///
161    /// [zip-0316]: https://zips.z.cash/zip-0316
162    const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_IVK;
163
164    /// The HRP for a Bech32m-encoded regtest Unified IVK.
165    const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK;
166
167    fn from_inner(ivks: Vec<Self::Item>) -> Self {
168        Self(ivks)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use alloc::borrow::ToOwned;
175    use alloc::vec::Vec;
176
177    use assert_matches::assert_matches;
178
179    use proptest::{
180        array::{uniform1, uniform32},
181        prelude::*,
182        sample::select,
183    };
184
185    use super::{Ivk, ParseError, Typecode, Uivk};
186    use crate::kind::unified::{
187        private::{SealedContainer, SealedItem},
188        Container, Encoding,
189    };
190    use zcash_protocol::consensus::NetworkType;
191
192    prop_compose! {
193        fn uniform64()(a in uniform32(0u8..), b in uniform32(0u8..)) -> [u8; 64] {
194            let mut c = [0; 64];
195            c[..32].copy_from_slice(&a);
196            c[32..].copy_from_slice(&b);
197            c
198        }
199    }
200
201    prop_compose! {
202        fn uniform65()(a in uniform1(0u8..), b in uniform64()) -> [u8; 65] {
203            let mut c = [0; 65];
204            c[..1].copy_from_slice(&a);
205            c[1..].copy_from_slice(&b);
206            c
207        }
208    }
209
210    fn arb_shielded_ivk() -> impl Strategy<Value = Vec<Ivk>> {
211        prop_oneof![
212            vec![uniform64().prop_map(Ivk::Sapling)],
213            vec![uniform64().prop_map(Ivk::Orchard)],
214            vec![
215                uniform64().prop_map(Ivk::Sapling as fn([u8; 64]) -> Ivk),
216                uniform64().prop_map(Ivk::Orchard)
217            ],
218        ]
219    }
220
221    fn arb_transparent_ivk() -> impl Strategy<Value = Ivk> {
222        uniform65().prop_map(Ivk::P2pkh)
223    }
224
225    prop_compose! {
226        fn arb_unified_ivk()(
227            shielded in arb_shielded_ivk(),
228            transparent in prop::option::of(arb_transparent_ivk()),
229        ) -> Uivk {
230            let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
231            items.sort_unstable_by(Ivk::encoding_order);
232            Uivk(items)
233        }
234    }
235
236    proptest! {
237        #[test]
238        fn uivk_roundtrip(
239            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
240            uivk in arb_unified_ivk(),
241        ) {
242            let encoded = uivk.encode(&network);
243            let decoded = Uivk::decode(&encoded);
244            prop_assert_eq!(decoded, Ok((network, uivk)));
245        }
246    }
247
248    #[test]
249    fn padding() {
250        // The test cases below use `Uivk(vec![Ivk::Orchard([1; 64])])` as base.
251
252        // Invalid padding ([0xff; 16] instead of [b'u', 0x00, 0x00, 0x00...])
253        let invalid_padding = [
254            0xba, 0xbc, 0xc0, 0x71, 0xcd, 0x3b, 0xfd, 0x9a, 0x32, 0x19, 0x7e, 0xeb, 0x8a, 0xa7,
255            0x6e, 0xd4, 0xac, 0xcb, 0x59, 0xc2, 0x54, 0x26, 0xc6, 0xab, 0x71, 0xc7, 0xc3, 0x72,
256            0xc, 0xa9, 0xad, 0xa4, 0xad, 0x8c, 0x9e, 0x35, 0x7b, 0x4c, 0x5d, 0xc7, 0x66, 0x12,
257            0x8a, 0xc5, 0x42, 0x89, 0xc1, 0x77, 0x32, 0xdc, 0xe8, 0x4b, 0x51, 0x31, 0x30, 0x3,
258            0x20, 0xe3, 0xb6, 0x8c, 0xbb, 0xab, 0xe8, 0x89, 0xf8, 0xed, 0xac, 0x6d, 0x8e, 0xb1,
259            0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
260        ];
261        assert_eq!(
262            Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]),
263            Err(ParseError::InvalidEncoding(
264                "Invalid padding bytes".to_owned()
265            ))
266        );
267
268        // Short padding (padded to 15 bytes instead of 16)
269        let truncated_padding = [
270            0x96, 0x73, 0x6a, 0x56, 0xbc, 0x44, 0x38, 0xe2, 0x47, 0x41, 0x1c, 0x70, 0xe4, 0x6,
271            0x87, 0xbe, 0xb6, 0x90, 0xbd, 0xab, 0x1b, 0xd8, 0x27, 0x10, 0x0, 0x21, 0x30, 0x2, 0x77,
272            0x87, 0x0, 0x25, 0x96, 0x94, 0x8f, 0x1e, 0x39, 0xd2, 0xd8, 0x65, 0xb4, 0x3c, 0x72,
273            0xd8, 0xac, 0xec, 0x5b, 0xa2, 0x18, 0x62, 0x3f, 0xb, 0x88, 0xb4, 0x41, 0xf1, 0x55,
274            0x39, 0x53, 0xbf, 0x2a, 0xd6, 0xcf, 0xdd, 0x46, 0xb7, 0xd8, 0xc1, 0x39, 0x34, 0x4d,
275            0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
276        ];
277        assert_eq!(
278            Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]),
279            Err(ParseError::InvalidEncoding(
280                "Invalid padding bytes".to_owned()
281            ))
282        );
283    }
284
285    #[test]
286    fn truncated() {
287        // The test cases below start from an encoding of
288        //     `Uivk(vec![Ivk::Orchard([1; 64]), Ivk::Sapling([2; 64])])`
289        // with the ivk data truncated, but valid padding.
290
291        // - Missing the last data byte of the Sapling ivk.
292        let truncated_sapling_data = [
293            0xce, 0xbc, 0xfe, 0xc5, 0xef, 0x2d, 0xe, 0x66, 0xc2, 0x8c, 0x34, 0xdc, 0x2e, 0x24,
294            0xd2, 0xc7, 0x4b, 0xac, 0x36, 0xe0, 0x43, 0x72, 0xa7, 0x33, 0xa4, 0xe, 0xe0, 0x52,
295            0x15, 0x64, 0x66, 0x92, 0x36, 0xa7, 0x60, 0x8e, 0x48, 0xe8, 0xb0, 0x30, 0x4d, 0xcb,
296            0xd, 0x6f, 0x5, 0xd4, 0xb8, 0x72, 0x6a, 0xdc, 0x6c, 0x5c, 0xa, 0xf8, 0xdf, 0x95, 0x5a,
297            0xba, 0xe1, 0xaa, 0x82, 0x51, 0xe2, 0x70, 0x8d, 0x13, 0x16, 0x88, 0x6a, 0xc0, 0xc1,
298            0x99, 0x3c, 0xaf, 0x2c, 0x16, 0x54, 0x80, 0x7e, 0xb, 0xad, 0x31, 0x29, 0x26, 0xdd,
299            0x7a, 0x55, 0x98, 0x1, 0x18, 0xb, 0x14, 0x94, 0xb2, 0x6b, 0x81, 0x67, 0x73, 0xa6, 0xd0,
300            0x20, 0x94, 0x17, 0x3a, 0xf9, 0x98, 0x43, 0x58, 0xd6, 0x1, 0x10, 0x73, 0x32, 0xb4,
301            0x99, 0xad, 0x6b, 0xfe, 0xc0, 0x97, 0xaf, 0xd2, 0xee, 0x8, 0xe5, 0x83, 0x6b, 0xb6,
302            0xd9, 0x0, 0xef, 0x84, 0xff, 0xe8, 0x58, 0xba, 0xe8, 0x10, 0xea, 0x2d, 0xee, 0x72,
303            0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
304        ];
305        assert_matches!(
306            Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]),
307            Err(ParseError::InvalidEncoding(_))
308        );
309
310        // - Truncated after the typecode of the Sapling ivk.
311        let truncated_after_sapling_typecode = [
312            0xf7, 0x3, 0xd8, 0xbe, 0x6a, 0x27, 0xfa, 0xa1, 0xd3, 0x11, 0xea, 0x25, 0x94, 0xe2, 0xb,
313            0xde, 0xed, 0x6a, 0xaa, 0x8, 0x46, 0x7d, 0xe4, 0xb1, 0xe, 0xf1, 0xde, 0x61, 0xd7, 0x95,
314            0xf7, 0x82, 0x62, 0x32, 0x7a, 0x73, 0x8c, 0x55, 0x93, 0xa1, 0x63, 0x75, 0xe2, 0xca,
315            0xcb, 0x73, 0xd5, 0xe5, 0xa3, 0xbd, 0xb3, 0xf2, 0x26, 0xfa, 0x1c, 0xa2, 0xad, 0xb6,
316            0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
317        ];
318        assert_matches!(
319            Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
320            Err(ParseError::InvalidEncoding(_))
321        );
322    }
323
324    #[test]
325    fn duplicate_typecode() {
326        // Construct and serialize an invalid UIVK.
327        let uivk = Uivk(vec![Ivk::Sapling([1; 64]), Ivk::Sapling([2; 64])]);
328        let encoded = uivk.encode(&NetworkType::Main);
329        assert_eq!(
330            Uivk::decode(&encoded),
331            Err(ParseError::DuplicateTypecode(Typecode::Sapling))
332        );
333    }
334
335    #[test]
336    fn only_transparent() {
337        // Raw Encoding of `Uivk(vec![Ivk::P2pkh([0; 65])])`.
338        let encoded = [
339            0x12, 0x51, 0x37, 0xc7, 0xac, 0x8c, 0xd, 0x13, 0x3a, 0x5f, 0xc6, 0x84, 0x53, 0x90,
340            0xf8, 0xe7, 0x23, 0x34, 0xfb, 0xda, 0x49, 0x3c, 0x87, 0x1c, 0x8f, 0x1a, 0xe1, 0x63,
341            0xba, 0xdf, 0x77, 0x64, 0x43, 0xcf, 0xdc, 0x37, 0x1f, 0xd2, 0x89, 0x60, 0xe3, 0x77,
342            0x20, 0xd0, 0x1c, 0x5, 0x40, 0xe5, 0x43, 0x55, 0xc4, 0xe5, 0xf8, 0xaa, 0xe, 0x7a, 0xe7,
343            0x8c, 0x53, 0x15, 0xb8, 0x8f, 0x90, 0x14, 0x33, 0x30, 0x52, 0x2b, 0x8, 0x89, 0x90,
344            0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64,
345        ];
346
347        assert_eq!(
348            Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
349            Err(ParseError::OnlyTransparent)
350        );
351    }
352
353    #[test]
354    fn ivks_are_sorted() {
355        // Construct a UIVK with ivks in an unsorted order.
356        let uivk = Uivk(vec![
357            Ivk::P2pkh([0; 65]),
358            Ivk::Orchard([0; 64]),
359            Ivk::Unknown {
360                typecode: 0xff,
361                data: vec![],
362            },
363            Ivk::Sapling([0; 64]),
364        ]);
365
366        // `Uivk::items` sorts the ivks in priority order.
367        assert_eq!(
368            uivk.items(),
369            vec![
370                Ivk::Orchard([0; 64]),
371                Ivk::Sapling([0; 64]),
372                Ivk::P2pkh([0; 65]),
373                Ivk::Unknown {
374                    typecode: 0xff,
375                    data: vec![],
376                },
377            ]
378        )
379    }
380
381    #[test]
382    fn ivk_debug_redaction() {
383        assert_eq!(
384            format!("{:?}", Ivk::Orchard([0; 64])),
385            "Ivk::Orchard(\"...\")"
386        );
387        assert_eq!(
388            format!("{:?}", Ivk::Sapling([0; 64])),
389            "Ivk::Sapling(\"...\")"
390        );
391        assert_eq!(format!("{:?}", Ivk::P2pkh([0; 65])), "Ivk::P2pkh(\"...\")");
392        assert_eq!(
393            format!(
394                "{:?}",
395                Ivk::Unknown {
396                    typecode: 31337,
397                    data: vec![4, 5, 6],
398                }
399            ),
400            "Ivk::Unknown { typecode: 31337, data: \"...\" }"
401        );
402    }
403
404    #[test]
405    fn uivk_debug_redaction() {
406        let uivk = Uivk(vec![
407            Ivk::Sapling([0; 64]),
408            Ivk::Unknown {
409                typecode: 9,
410                data: vec![8, 8, 8],
411            },
412        ]);
413
414        assert_eq!(
415            format!("{:?}", uivk),
416            "Uivk([Ivk::Sapling(\"...\"), Ivk::Unknown { typecode: 9, data: \"...\" }])"
417        );
418    }
419}