Skip to main content

zcash_address/kind/unified/
ivk.rs

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