Skip to main content

zcash_address/kind/unified/
ivk.rs

1use alloc::vec::Vec;
2use core::{convert::TryInto, fmt};
3
4use super::{
5    Container, DataTypecode, Encoding, P2shItemKind, ParseError, Uitem,
6    private::{SealedContainer, SealedItem, validate_p2sh_item},
7};
8use crate::{constants, unified::Revision};
9
10/// The set of known IVKs for Unified IVKs.
11#[derive(Clone, PartialEq, Eq, Hash)]
12pub enum Ivk {
13    /// The raw encoding of an Orchard Incoming Viewing Key.
14    ///
15    /// `(dk, ivk)` each 32 bytes.
16    Orchard([u8; 64]),
17
18    /// Data contained within the Sapling component of a Unified Incoming Viewing Key.
19    ///
20    /// In order to ensure that Unified Addresses can always be derived from UIVKs, we
21    /// store more data here than was specified to be part of a Sapling IVK. Specifically,
22    /// we store the same data here as we do for Orchard.
23    ///
24    /// `(dk, ivk)` each 32 bytes.
25    Sapling([u8; 64]),
26
27    /// A pruned version of the extended public key for the BIP 44 account corresponding to the
28    /// transparent address subtree from which transparent addresses are derived,
29    /// at the external `change` BIP 44 path, i.e. `m/44'/133'/<account_id>'/0`. This
30    /// includes just the chain code (32 bytes) and the compressed public key (33 bytes), and excludes
31    /// the depth of in the derivation tree, the parent key fingerprint, and the child key
32    /// number (which would reveal the wallet account number for which this UFVK was generated).
33    ///
34    /// Transparent addresses don't have "viewing keys" - the addresses themselves serve
35    /// that purpose. However, we want the ability to derive diversified Unified Addresses
36    /// from Unified Viewing Keys, and to not break the unlinkability property when they
37    /// include transparent receivers. To achieve this, we treat the last hardened node in
38    /// the BIP 44 derivation path as the "transparent viewing key"; all addresses derived
39    /// from this node use non-hardened derivation, and can thus be derived just from this
40    /// pruned extended public key.
41    P2pkh([u8; 65]),
42
43    /// The payload of a P2SH viewing key item: the canonical encoding of a [BIP 388]
44    /// wallet policy (a descriptor template using `/*` multipath notation, followed by a
45    /// key information vector), as specified for [ZIP 316] Revision 2.
46    ///
47    /// The payload is structurally validated during parsing; interpretation of the
48    /// policy is the responsibility of the consumer.
49    ///
50    /// [BIP 388]: https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki
51    /// [ZIP 316]: https://zips.z.cash/zip-0316
52    P2sh(Vec<u8>),
53
54    Unknown {
55        typecode: u32,
56        data: Vec<u8>,
57    },
58}
59
60impl fmt::Debug for Ivk {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Ivk::Orchard(_) => f.debug_tuple("Ivk::Orchard").field(&"...").finish(),
64            Ivk::Sapling(_) => f.debug_tuple("Ivk::Sapling").field(&"...").finish(),
65            Ivk::P2pkh(_) => f.debug_tuple("Ivk::P2pkh").field(&"...").finish(),
66            Ivk::P2sh(_) => f.debug_tuple("Ivk::P2sh").field(&"...").finish(),
67            Ivk::Unknown { typecode, .. } => f
68                .debug_struct("Ivk::Unknown")
69                .field("typecode", typecode)
70                .field("data", &"...")
71                .finish(),
72        }
73    }
74}
75
76impl SealedItem for Ivk {
77    fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
78        if typecode == DataTypecode::P2sh {
79            validate_p2sh_item(P2shItemKind::IncomingViewing, data)
80                .map_err(ParseError::InvalidP2shItem)?;
81            return Ok(Ivk::P2sh(data.to_vec()));
82        }
83        let data = data.to_vec();
84        match typecode {
85            DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh),
86            DataTypecode::P2sh => unreachable!("handled above"),
87            DataTypecode::Sapling => data.try_into().map(Ivk::Sapling),
88            DataTypecode::Orchard => data.try_into().map(Ivk::Orchard),
89            DataTypecode::Unknown(tc) => Ok(Ivk::Unknown { typecode: tc, data }),
90        }
91        .map_err(|e| {
92            ParseError::InvalidEncoding(format!(
93                "Invalid ivk for typecode {}: {e:?}",
94                u32::from(typecode)
95            ))
96        })
97    }
98
99    fn typecode(&self) -> DataTypecode {
100        match self {
101            Ivk::P2pkh(_) => DataTypecode::P2pkh,
102            Ivk::P2sh(_) => DataTypecode::P2sh,
103            Ivk::Sapling(_) => DataTypecode::Sapling,
104            Ivk::Orchard(_) => DataTypecode::Orchard,
105            Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
106        }
107    }
108
109    fn data(&self) -> &[u8] {
110        match self {
111            Ivk::P2pkh(data) => data,
112            Ivk::P2sh(data) => data,
113            Ivk::Sapling(data) => data,
114            Ivk::Orchard(data) => data,
115            Ivk::Unknown { data, .. } => data,
116        }
117    }
118}
119
120/// A Unified Incoming Viewing Key.
121///
122/// # Examples
123///
124/// ```
125/// use zcash_address::unified::{self, Container, Encoding, Uitem};
126///
127/// # #[cfg(not(feature = "std"))]
128/// # fn main() {}
129/// # #[cfg(feature = "std")]
130/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
131/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
132/// let example_uivk: &str = uivk_from_user();
133///
134/// let (network, _revision, uivk) = unified::Uivk::decode(example_uivk)?;
135///
136/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in
137/// // preference order (the order in which wallets should prefer to use their
138/// // corresponding address receivers):
139/// let ivks: Vec<unified::Ivk> = uivk.items();
140///
141/// // And we can create the UIVK from a list of IVKs:
142/// let new_uivk = unified::Uivk::try_from_items(
143///     unified::Revision::R0,
144///     ivks.into_iter().map(Uitem::Data).collect(),
145/// )?;
146/// assert_eq!(new_uivk, uivk);
147/// # Ok(())
148/// # }
149/// ```
150#[derive(Clone, Debug, PartialEq, Eq, Hash)]
151pub struct Uivk {
152    pub(crate) revision: Revision,
153    pub(crate) items: Vec<Uitem<Ivk>>,
154}
155
156impl Container for Uivk {
157    type Item = Ivk;
158
159    fn revision(&self) -> Revision {
160        self.revision
161    }
162
163    /// Returns the IVKs and metadata items contained within this UIVK, in the order they
164    /// were parsed from the string encoding.
165    ///
166    /// This API is for advanced usage; in most cases you should use `Uivk::items`.
167    fn items_as_parsed(&self) -> &[Uitem<Ivk>] {
168        &self.items
169    }
170}
171
172impl Encoding for Uivk {}
173
174impl SealedContainer for Uivk {
175    const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_IVK;
176    const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_IVK;
177    const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK;
178
179    const MAINNET_R2: &'static str = constants::mainnet::HRP_UNIFIED_IVK_R2;
180    const TESTNET_R2: &'static str = constants::testnet::HRP_UNIFIED_IVK_R2;
181    const REGTEST_R2: &'static str = constants::regtest::HRP_UNIFIED_IVK_R2;
182
183    const MAINNET_R2_TI: &'static str = constants::mainnet::HRP_UNIFIED_IVK_R2;
184    const TESTNET_R2_TI: &'static str = constants::testnet::HRP_UNIFIED_IVK_R2;
185    const REGTEST_R2_TI: &'static str = constants::regtest::HRP_UNIFIED_IVK_R2;
186
187    const IS_ADDRESS: bool = false;
188
189    fn from_inner(revision: Revision, items: Vec<Uitem<Ivk>>) -> Self {
190        Self { revision, items }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use alloc::borrow::ToOwned;
197    use alloc::vec::Vec;
198
199    use assert_matches::assert_matches;
200
201    use proptest::{
202        array::{uniform1, uniform32},
203        prelude::*,
204        sample::select,
205    };
206
207    use super::{Ivk, ParseError, Uivk};
208    use crate::kind::unified::{
209        Container, DataTypecode, Encoding, Revision, Typecode, Uitem, private::SealedContainer,
210    };
211    use zcash_protocol::consensus::NetworkType;
212
213    prop_compose! {
214        fn uniform64()(a in uniform32(0u8..), b in uniform32(0u8..)) -> [u8; 64] {
215            let mut c = [0; 64];
216            c[..32].copy_from_slice(&a);
217            c[32..].copy_from_slice(&b);
218            c
219        }
220    }
221
222    prop_compose! {
223        fn uniform65()(a in uniform1(0u8..), b in uniform64()) -> [u8; 65] {
224            let mut c = [0; 65];
225            c[..1].copy_from_slice(&a);
226            c[1..].copy_from_slice(&b);
227            c
228        }
229    }
230
231    fn arb_shielded_ivk() -> impl Strategy<Value = Vec<Ivk>> {
232        prop_oneof![
233            vec![uniform64().prop_map(Ivk::Sapling)],
234            vec![uniform64().prop_map(Ivk::Orchard)],
235            vec![
236                uniform64().prop_map(Ivk::Sapling as fn([u8; 64]) -> Ivk),
237                uniform64().prop_map(Ivk::Orchard)
238            ],
239        ]
240    }
241
242    fn arb_transparent_ivk() -> impl Strategy<Value = Ivk> {
243        uniform65().prop_map(Ivk::P2pkh)
244    }
245
246    prop_compose! {
247        fn arb_unified_ivk()(
248            shielded in arb_shielded_ivk(),
249            transparent in prop::option::of(arb_transparent_ivk()),
250        ) -> Uivk {
251            let mut items: Vec<Uitem<Ivk>> = transparent
252                .into_iter()
253                .chain(shielded)
254                .map(Uitem::Data)
255                .collect();
256            items.sort_unstable_by(Uitem::encoding_order);
257            Uivk {
258                revision: Revision::R0,
259                items,
260            }
261        }
262    }
263
264    use crate::kind::unified::MetadataItem;
265
266    fn arb_metadata_items() -> impl Strategy<Value = Vec<Uitem<Ivk>>> {
267        (
268            prop::option::of(
269                any::<u32>().prop_map(|h| Uitem::Metadata(MetadataItem::ExpiryHeight(h))),
270            ),
271            prop::option::of(
272                any::<u64>().prop_map(|t| Uitem::Metadata(MetadataItem::ExpiryTime(t))),
273            ),
274        )
275            .prop_map(|(h, t)| h.into_iter().chain(t).collect())
276    }
277
278    prop_compose! {
279        fn arb_r2_unified_ivk()(
280            shielded in arb_shielded_ivk(),
281            transparent in prop::option::of(arb_transparent_ivk()),
282            metadata in arb_metadata_items(),
283        ) -> Uivk {
284            let mut items: Vec<Uitem<Ivk>> = transparent
285                .into_iter()
286                .chain(shielded)
287                .map(Uitem::Data)
288                .chain(metadata)
289                .collect();
290            items.sort_unstable_by(Uitem::encoding_order);
291            Uivk {
292                revision: Revision::R2,
293                items,
294            }
295        }
296    }
297
298    // R2 allows transparent-only UIVKs.
299    prop_compose! {
300        fn arb_r2_transparent_only_ivk()(
301            transparent in arb_transparent_ivk(),
302            metadata in arb_metadata_items(),
303        ) -> Uivk {
304            let mut items: Vec<Uitem<Ivk>> = core::iter::once(Uitem::Data(transparent))
305                .chain(metadata)
306                .collect();
307            items.sort_unstable_by(Uitem::encoding_order);
308            Uivk {
309                revision: Revision::R2,
310                items,
311            }
312        }
313    }
314
315    proptest! {
316        #[test]
317        fn uivk_roundtrip(
318            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
319            uivk in arb_unified_ivk(),
320        ) {
321            let encoded = uivk.encode(&network);
322            let decoded = Uivk::decode(&encoded);
323            let decoded = decoded.map(|(net, _rev, uivk)| (net, uivk));
324            prop_assert_eq!(decoded, Ok((network, uivk)));
325        }
326
327        #[test]
328        fn r2_uivk_roundtrip(
329            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
330            uivk in arb_r2_unified_ivk(),
331        ) {
332            let encoded = uivk.encode(&network);
333            let decoded = Uivk::decode(&encoded);
334            let decoded = decoded.map(|(net, _rev, uivk)| (net, uivk));
335            prop_assert_eq!(decoded, Ok((network, uivk)));
336        }
337
338        #[test]
339        fn r2_transparent_only_uivk_roundtrip(
340            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
341            uivk in arb_r2_transparent_only_ivk(),
342        ) {
343            let encoded = uivk.encode(&network);
344            let decoded = Uivk::decode(&encoded);
345            let decoded = decoded.map(|(net, _rev, uivk)| (net, uivk));
346            prop_assert_eq!(decoded, Ok((network, uivk)));
347        }
348    }
349
350    #[test]
351    fn padding() {
352        // Invalid padding ([0xff; 16] instead of [b'u', 0x00, 0x00, 0x00...])
353        let invalid_padding = [
354            0xba, 0xbc, 0xc0, 0x71, 0xcd, 0x3b, 0xfd, 0x9a, 0x32, 0x19, 0x7e, 0xeb, 0x8a, 0xa7,
355            0x6e, 0xd4, 0xac, 0xcb, 0x59, 0xc2, 0x54, 0x26, 0xc6, 0xab, 0x71, 0xc7, 0xc3, 0x72,
356            0xc, 0xa9, 0xad, 0xa4, 0xad, 0x8c, 0x9e, 0x35, 0x7b, 0x4c, 0x5d, 0xc7, 0x66, 0x12,
357            0x8a, 0xc5, 0x42, 0x89, 0xc1, 0x77, 0x32, 0xdc, 0xe8, 0x4b, 0x51, 0x31, 0x30, 0x3,
358            0x20, 0xe3, 0xb6, 0x8c, 0xbb, 0xab, 0xe8, 0x89, 0xf8, 0xed, 0xac, 0x6d, 0x8e, 0xb1,
359            0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
360        ];
361        assert_eq!(
362            Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..], Revision::R0),
363            Err(ParseError::InvalidEncoding(
364                "Invalid padding bytes".to_owned()
365            ))
366        );
367
368        // Short padding (padded to 15 bytes instead of 16)
369        let truncated_padding = [
370            0x96, 0x73, 0x6a, 0x56, 0xbc, 0x44, 0x38, 0xe2, 0x47, 0x41, 0x1c, 0x70, 0xe4, 0x6,
371            0x87, 0xbe, 0xb6, 0x90, 0xbd, 0xab, 0x1b, 0xd8, 0x27, 0x10, 0x0, 0x21, 0x30, 0x2, 0x77,
372            0x87, 0x0, 0x25, 0x96, 0x94, 0x8f, 0x1e, 0x39, 0xd2, 0xd8, 0x65, 0xb4, 0x3c, 0x72,
373            0xd8, 0xac, 0xec, 0x5b, 0xa2, 0x18, 0x62, 0x3f, 0xb, 0x88, 0xb4, 0x41, 0xf1, 0x55,
374            0x39, 0x53, 0xbf, 0x2a, 0xd6, 0xcf, 0xdd, 0x46, 0xb7, 0xd8, 0xc1, 0x39, 0x34, 0x4d,
375            0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
376        ];
377        assert_eq!(
378            Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..], Revision::R0),
379            Err(ParseError::InvalidEncoding(
380                "Invalid padding bytes".to_owned()
381            ))
382        );
383    }
384
385    #[test]
386    fn truncated() {
387        // - Missing the last data byte of the Sapling ivk.
388        let truncated_sapling_data = [
389            0xce, 0xbc, 0xfe, 0xc5, 0xef, 0x2d, 0xe, 0x66, 0xc2, 0x8c, 0x34, 0xdc, 0x2e, 0x24,
390            0xd2, 0xc7, 0x4b, 0xac, 0x36, 0xe0, 0x43, 0x72, 0xa7, 0x33, 0xa4, 0xe, 0xe0, 0x52,
391            0x15, 0x64, 0x66, 0x92, 0x36, 0xa7, 0x60, 0x8e, 0x48, 0xe8, 0xb0, 0x30, 0x4d, 0xcb,
392            0xd, 0x6f, 0x5, 0xd4, 0xb8, 0x72, 0x6a, 0xdc, 0x6c, 0x5c, 0xa, 0xf8, 0xdf, 0x95, 0x5a,
393            0xba, 0xe1, 0xaa, 0x82, 0x51, 0xe2, 0x70, 0x8d, 0x13, 0x16, 0x88, 0x6a, 0xc0, 0xc1,
394            0x99, 0x3c, 0xaf, 0x2c, 0x16, 0x54, 0x80, 0x7e, 0xb, 0xad, 0x31, 0x29, 0x26, 0xdd,
395            0x7a, 0x55, 0x98, 0x1, 0x18, 0xb, 0x14, 0x94, 0xb2, 0x6b, 0x81, 0x67, 0x73, 0xa6, 0xd0,
396            0x20, 0x94, 0x17, 0x3a, 0xf9, 0x98, 0x43, 0x58, 0xd6, 0x1, 0x10, 0x73, 0x32, 0xb4,
397            0x99, 0xad, 0x6b, 0xfe, 0xc0, 0x97, 0xaf, 0xd2, 0xee, 0x8, 0xe5, 0x83, 0x6b, 0xb6,
398            0xd9, 0x0, 0xef, 0x84, 0xff, 0xe8, 0x58, 0xba, 0xe8, 0x10, 0xea, 0x2d, 0xee, 0x72,
399            0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
400        ];
401        assert_matches!(
402            Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..], Revision::R0),
403            Err(ParseError::InvalidEncoding(_))
404        );
405
406        // - Truncated after the typecode of the Sapling ivk.
407        let truncated_after_sapling_typecode = [
408            0xf7, 0x3, 0xd8, 0xbe, 0x6a, 0x27, 0xfa, 0xa1, 0xd3, 0x11, 0xea, 0x25, 0x94, 0xe2, 0xb,
409            0xde, 0xed, 0x6a, 0xaa, 0x8, 0x46, 0x7d, 0xe4, 0xb1, 0xe, 0xf1, 0xde, 0x61, 0xd7, 0x95,
410            0xf7, 0x82, 0x62, 0x32, 0x7a, 0x73, 0x8c, 0x55, 0x93, 0xa1, 0x63, 0x75, 0xe2, 0xca,
411            0xcb, 0x73, 0xd5, 0xe5, 0xa3, 0xbd, 0xb3, 0xf2, 0x26, 0xfa, 0x1c, 0xa2, 0xad, 0xb6,
412            0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
413        ];
414        assert_matches!(
415            Uivk::parse_internal(
416                Uivk::MAINNET,
417                &truncated_after_sapling_typecode[..],
418                Revision::R0
419            ),
420            Err(ParseError::InvalidEncoding(_))
421        );
422    }
423
424    #[test]
425    fn duplicate_typecode() {
426        let uivk = Uivk {
427            revision: Revision::R0,
428            items: vec![
429                Uitem::Data(Ivk::Sapling([1; 64])),
430                Uitem::Data(Ivk::Sapling([2; 64])),
431            ],
432        };
433        let encoded = uivk.encode(&NetworkType::Main);
434        assert_eq!(
435            Uivk::decode(&encoded).map(|(net, _rev, uivk)| (net, uivk)),
436            Err(ParseError::DuplicateTypecode(Typecode::Data(
437                DataTypecode::Sapling
438            )))
439        );
440    }
441
442    #[test]
443    fn only_transparent() {
444        // Raw Encoding of `Uivk { items: vec![Uitem::Data(Ivk::P2pkh([0; 65]))] }`.
445        let encoded = [
446            0x12, 0x51, 0x37, 0xc7, 0xac, 0x8c, 0xd, 0x13, 0x3a, 0x5f, 0xc6, 0x84, 0x53, 0x90,
447            0xf8, 0xe7, 0x23, 0x34, 0xfb, 0xda, 0x49, 0x3c, 0x87, 0x1c, 0x8f, 0x1a, 0xe1, 0x63,
448            0xba, 0xdf, 0x77, 0x64, 0x43, 0xcf, 0xdc, 0x37, 0x1f, 0xd2, 0x89, 0x60, 0xe3, 0x77,
449            0x20, 0xd0, 0x1c, 0x5, 0x40, 0xe5, 0x43, 0x55, 0xc4, 0xe5, 0xf8, 0xaa, 0xe, 0x7a, 0xe7,
450            0x8c, 0x53, 0x15, 0xb8, 0x8f, 0x90, 0x14, 0x33, 0x30, 0x52, 0x2b, 0x8, 0x89, 0x90,
451            0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64,
452        ];
453
454        assert_eq!(
455            Uivk::parse_internal(Uivk::MAINNET, &encoded[..], Revision::R0),
456            Err(ParseError::OnlyTransparent)
457        );
458    }
459
460    #[test]
461    fn ivks_are_sorted() {
462        let uivk = Uivk {
463            revision: Revision::R0,
464            items: vec![
465                Uitem::Data(Ivk::P2pkh([0; 65])),
466                Uitem::Data(Ivk::Orchard([0; 64])),
467                Uitem::Data(Ivk::Unknown {
468                    typecode: 0x50,
469                    data: vec![],
470                }),
471                Uitem::Data(Ivk::Sapling([0; 64])),
472            ],
473        };
474
475        assert_eq!(
476            uivk.items(),
477            vec![
478                Ivk::Orchard([0; 64]),
479                Ivk::Sapling([0; 64]),
480                Ivk::P2pkh([0; 65]),
481                Ivk::Unknown {
482                    typecode: 0x50,
483                    data: vec![],
484                },
485            ]
486        )
487    }
488
489    #[test]
490    fn ivk_debug_redaction() {
491        assert_eq!(
492            format!("{:?}", Ivk::Orchard([0; 64])),
493            "Ivk::Orchard(\"...\")"
494        );
495        assert_eq!(
496            format!("{:?}", Ivk::Sapling([0; 64])),
497            "Ivk::Sapling(\"...\")"
498        );
499        assert_eq!(format!("{:?}", Ivk::P2pkh([0; 65])), "Ivk::P2pkh(\"...\")");
500        assert_eq!(
501            format!(
502                "{:?}",
503                Ivk::Unknown {
504                    typecode: 31337,
505                    data: vec![4, 5, 6],
506                }
507            ),
508            "Ivk::Unknown { typecode: 31337, data: \"...\" }"
509        );
510    }
511
512    #[test]
513    fn uivk_debug_redaction() {
514        let uivk = Uivk {
515            revision: Revision::R0,
516            items: vec![
517                Uitem::Data(Ivk::Sapling([0; 64])),
518                Uitem::Data(Ivk::Unknown {
519                    typecode: 9,
520                    data: vec![8, 8, 8],
521                }),
522            ],
523        };
524
525        assert_eq!(
526            format!("{uivk:?}"),
527            "Uivk { revision: R0, items: [Data(Ivk::Sapling(\"...\")), Data(Ivk::Unknown { typecode: 9, data: \"...\" })] }"
528        );
529    }
530
531    #[test]
532    fn p2sh_ivk_item_parsing() {
533        use crate::kind::unified::{P2shItemError, ParseError, private::SealedItem};
534
535        /// Chain code plus compressed pubkey.
536        const KEY_INFO_LEN: usize = 65;
537        let payload = |template: &str, n_keys: usize| {
538            let mut payload = vec![];
539            zcash_encoding::CompactSize::write(&mut payload, template.len()).unwrap();
540            payload.extend_from_slice(template.as_bytes());
541            zcash_encoding::CompactSize::write(&mut payload, n_keys).unwrap();
542            payload.extend_from_slice(&vec![0u8; n_keys * KEY_INFO_LEN]);
543            payload
544        };
545
546        let valid = payload("sh(sortedmulti(2,@0/*,@1/*,@2/*))", 3);
547        assert_eq!(
548            Ivk::parse(DataTypecode::P2sh, &valid),
549            Ok(Ivk::P2sh(valid.clone()))
550        );
551
552        // A UIVK template must use `/*` notation, not the `/**` multipath notation.
553        let fvk_notation = payload("sh(sortedmulti(2,@0/**,@1/**,@2/**))", 3);
554        assert_eq!(
555            Ivk::parse(DataTypecode::P2sh, &fvk_notation),
556            Err(ParseError::InvalidP2shItem(P2shItemError::Multipath))
557        );
558    }
559
560    #[test]
561    fn r0_uivk_treats_p2sh_item_as_unrecognised() {
562        // ZIP 316 gives Typecode 0x01 no meaning in a Revision 0 UIVK, so a consumer
563        // must retain it as an unrecognised item rather than interpret it as a P2SH
564        // viewing key item. This holds whether or not the payload happens to be a
565        // well-formed BIP 388 wallet policy.
566        const P2SH_TYPECODE: u32 = 0x01;
567        /// Chain code plus compressed pubkey.
568        const KEY_INFO_LEN: usize = 65;
569
570        let policy = |template: &str, n_keys: usize| {
571            let mut payload = vec![];
572            zcash_encoding::CompactSize::write(&mut payload, template.len()).unwrap();
573            payload.extend_from_slice(template.as_bytes());
574            zcash_encoding::CompactSize::write(&mut payload, n_keys).unwrap();
575            payload.extend_from_slice(&vec![0u8; n_keys * KEY_INFO_LEN]);
576            payload
577        };
578
579        for payload in [
580            policy("sh(sortedmulti(2,@0/*,@1/*,@2/*))", 3),
581            b"not a wallet policy".to_vec(),
582        ] {
583            let uivk = Uivk::try_from_items(
584                Revision::R0,
585                vec![
586                    Uitem::Data(Ivk::Unknown {
587                        typecode: P2SH_TYPECODE,
588                        data: payload.clone(),
589                    }),
590                    Uitem::Data(Ivk::Sapling([0; 64])),
591                ],
592            )
593            .unwrap();
594
595            let (_, revision, decoded) = Uivk::decode(&uivk.encode(&NetworkType::Main)).unwrap();
596            assert_eq!(revision, Revision::R0);
597            assert!(decoded.items().contains(&Ivk::Unknown {
598                typecode: P2SH_TYPECODE,
599                data: payload,
600            }));
601            assert!(
602                !decoded
603                    .items()
604                    .iter()
605                    .any(|ivk| matches!(ivk, Ivk::P2sh(_)))
606            );
607        }
608    }
609}