Skip to main content

zcash_address/kind/unified/
fvk.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 FVKs for Unified FVKs.
11#[derive(Clone, PartialEq, Eq, Hash)]
12pub enum Fvk {
13    /// The raw encoding of an Orchard Full Viewing Key.
14    ///
15    /// `(ak, nk, rivk)` each 32 bytes.
16    Orchard([u8; 96]),
17
18    /// Data contained within the Sapling component of a Unified Full Viewing Key
19    ///
20    /// `(ak, nk, ovk, dk)` each 32 bytes.
21    Sapling([u8; 128]),
22
23    /// A pruned version of the extended public key for the BIP 44 account corresponding to the
24    /// transparent address subtree from which transparent addresses are derived. This
25    /// includes just the chain code (32 bytes) and the compressed public key (33 bytes), and excludes
26    /// the depth of in the derivation tree, the parent key fingerprint, and the child key
27    /// number (which would reveal the wallet account number for which this UFVK was generated).
28    ///
29    /// Transparent addresses don't have "viewing keys" - the addresses themselves serve
30    /// that purpose. However, we want the ability to derive diversified Unified Addresses
31    /// from Unified Viewing Keys, and to not break the unlinkability property when they
32    /// include transparent receivers. To achieve this, we treat the last hardened node in
33    /// the BIP 44 derivation path as the "transparent viewing key"; all addresses derived
34    /// from this node use non-hardened derivation, and can thus be derived just from this
35    /// pruned extended public key.
36    P2pkh([u8; 65]),
37
38    /// The payload of a P2SH viewing key item: the canonical encoding of a [BIP 388]
39    /// wallet policy (a descriptor template using `/**` multipath notation, followed by
40    /// a key information vector), as specified for [ZIP 316] Revision 2.
41    ///
42    /// The payload is structurally validated during parsing; interpretation of the
43    /// policy is the responsibility of the consumer.
44    ///
45    /// [BIP 388]: https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki
46    /// [ZIP 316]: https://zips.z.cash/zip-0316
47    P2sh(Vec<u8>),
48
49    Unknown {
50        typecode: u32,
51        data: Vec<u8>,
52    },
53}
54
55impl fmt::Debug for Fvk {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Fvk::Orchard(_) => f.debug_tuple("Fvk::Orchard").field(&"...").finish(),
59            Fvk::Sapling(_) => f.debug_tuple("Fvk::Sapling").field(&"...").finish(),
60            Fvk::P2pkh(_) => f.debug_tuple("Fvk::P2pkh").field(&"...").finish(),
61            Fvk::P2sh(_) => f.debug_tuple("Fvk::P2sh").field(&"...").finish(),
62            Fvk::Unknown { typecode, .. } => f
63                .debug_struct("Fvk::Unknown")
64                .field("typecode", typecode)
65                .field("data", &"...")
66                .finish(),
67        }
68    }
69}
70
71impl SealedItem for Fvk {
72    fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
73        if typecode == DataTypecode::P2sh {
74            validate_p2sh_item(P2shItemKind::FullViewing, data)
75                .map_err(ParseError::InvalidP2shItem)?;
76            return Ok(Fvk::P2sh(data.to_vec()));
77        }
78        let data = data.to_vec();
79        match typecode {
80            DataTypecode::P2pkh => data.try_into().map(Fvk::P2pkh),
81            DataTypecode::P2sh => unreachable!("handled above"),
82            DataTypecode::Sapling => data.try_into().map(Fvk::Sapling),
83            DataTypecode::Orchard => data.try_into().map(Fvk::Orchard),
84            DataTypecode::Unknown(tc) => Ok(Fvk::Unknown { typecode: tc, data }),
85        }
86        .map_err(|e| {
87            ParseError::InvalidEncoding(format!(
88                "Invalid fvk for typecode {}: {e:?}",
89                u32::from(typecode)
90            ))
91        })
92    }
93
94    fn typecode(&self) -> DataTypecode {
95        match self {
96            Fvk::P2pkh(_) => DataTypecode::P2pkh,
97            Fvk::P2sh(_) => DataTypecode::P2sh,
98            Fvk::Sapling(_) => DataTypecode::Sapling,
99            Fvk::Orchard(_) => DataTypecode::Orchard,
100            Fvk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
101        }
102    }
103
104    fn data(&self) -> &[u8] {
105        match self {
106            Fvk::P2pkh(data) => data,
107            Fvk::P2sh(data) => data,
108            Fvk::Sapling(data) => data,
109            Fvk::Orchard(data) => data,
110            Fvk::Unknown { data, .. } => data,
111        }
112    }
113}
114
115/// A Unified Full Viewing Key.
116///
117/// # Examples
118///
119/// ```
120/// use zcash_address::unified::{self, Container, Encoding, Uitem};
121///
122/// # #[cfg(not(feature = "std"))]
123/// # fn main() {}
124/// # #[cfg(feature = "std")]
125/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
126/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
127/// let example_ufvk: &str = ufvk_from_user();
128///
129/// let (network, _revision, ufvk) = unified::Ufvk::decode(example_ufvk)?;
130///
131/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference
132/// // order (the order in which wallets should prefer to use their corresponding
133/// // address receivers):
134/// let fvks: Vec<unified::Fvk> = ufvk.items();
135///
136/// // And we can create the UFVK from a list of FVKs:
137/// let new_ufvk = unified::Ufvk::try_from_items(
138///     unified::Revision::R0,
139///     fvks.into_iter().map(Uitem::Data).collect(),
140/// )?;
141/// assert_eq!(new_ufvk, ufvk);
142/// # Ok(())
143/// # }
144/// ```
145#[derive(Clone, Debug, PartialEq, Eq, Hash)]
146pub struct Ufvk {
147    pub(crate) revision: Revision,
148    pub(crate) items: Vec<Uitem<Fvk>>,
149}
150
151impl Container for Ufvk {
152    type Item = Fvk;
153
154    fn revision(&self) -> Revision {
155        self.revision
156    }
157
158    /// Returns the FVKs contained within this UFVK, in the order they were
159    /// parsed from the string encoding.
160    ///
161    /// This API is for advanced usage; in most cases you should use `Ufvk::items`.
162    fn items_as_parsed(&self) -> &[Uitem<Fvk>] {
163        &self.items
164    }
165}
166
167impl Encoding for Ufvk {}
168
169impl SealedContainer for Ufvk {
170    const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_FVK;
171    const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_FVK;
172    const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_FVK;
173
174    const MAINNET_R2: &'static str = constants::mainnet::HRP_UNIFIED_FVK_R2;
175    const TESTNET_R2: &'static str = constants::testnet::HRP_UNIFIED_FVK_R2;
176    const REGTEST_R2: &'static str = constants::regtest::HRP_UNIFIED_FVK_R2;
177
178    const MAINNET_R2_TI: &'static str = constants::mainnet::HRP_UNIFIED_FVK_R2;
179    const TESTNET_R2_TI: &'static str = constants::testnet::HRP_UNIFIED_FVK_R2;
180    const REGTEST_R2_TI: &'static str = constants::regtest::HRP_UNIFIED_FVK_R2;
181
182    const IS_ADDRESS: bool = false;
183
184    fn from_inner(revision: Revision, items: Vec<Uitem<Fvk>>) -> Self {
185        Self { revision, items }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use alloc::borrow::ToOwned;
192    use alloc::vec::Vec;
193
194    use assert_matches::assert_matches;
195
196    use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select};
197
198    use super::{Fvk, ParseError, Ufvk};
199    use crate::kind::unified::{
200        Container, DataTypecode, Encoding, MetadataItem, Revision, Typecode, Uitem,
201        private::SealedContainer,
202    };
203    use zcash_protocol::consensus::NetworkType;
204
205    prop_compose! {
206        fn uniform128()(a in uniform96(), b in uniform32(0u8..)) -> [u8; 128] {
207            let mut fvk = [0; 128];
208            fvk[..96].copy_from_slice(&a);
209            fvk[96..].copy_from_slice(&b);
210            fvk
211        }
212    }
213
214    prop_compose! {
215        fn uniform96()(a in uniform32(0u8..), b in uniform32(0u8..), c in uniform32(0u8..)) -> [u8; 96] {
216            let mut fvk = [0; 96];
217            fvk[..32].copy_from_slice(&a);
218            fvk[32..64].copy_from_slice(&b);
219            fvk[64..].copy_from_slice(&c);
220            fvk
221        }
222    }
223
224    prop_compose! {
225        fn uniform65()(a in uniform32(0u8..), b in uniform32(0u8..), c in uniform1(0u8..)) -> [u8; 65] {
226            let mut fvk = [0; 65];
227            fvk[..32].copy_from_slice(&a);
228            fvk[32..64].copy_from_slice(&b);
229            fvk[64..].copy_from_slice(&c);
230            fvk
231        }
232    }
233
234    pub fn arb_orchard_fvk() -> impl Strategy<Value = Fvk> {
235        uniform96().prop_map(Fvk::Orchard)
236    }
237
238    pub fn arb_sapling_fvk() -> impl Strategy<Value = Fvk> {
239        uniform128().prop_map(Fvk::Sapling)
240    }
241
242    fn arb_shielded_fvk() -> impl Strategy<Value = Vec<Fvk>> {
243        prop_oneof![
244            vec![arb_sapling_fvk().boxed()],
245            vec![arb_orchard_fvk().boxed()],
246            vec![arb_sapling_fvk().boxed(), arb_orchard_fvk().boxed()],
247        ]
248    }
249
250    fn arb_transparent_fvk() -> BoxedStrategy<Fvk> {
251        uniform65().prop_map(Fvk::P2pkh).boxed()
252    }
253
254    prop_compose! {
255        fn arb_unified_fvk()(
256            shielded in arb_shielded_fvk(),
257            transparent in prop::option::of(arb_transparent_fvk()),
258        ) -> Ufvk {
259            let mut items: Vec<Uitem<Fvk>> = transparent
260                .into_iter()
261                .chain(shielded)
262                .map(Uitem::Data)
263                .collect();
264            items.sort_unstable_by(Uitem::encoding_order);
265            Ufvk {
266                revision: Revision::R0,
267                items,
268            }
269        }
270    }
271
272    fn arb_metadata_items() -> impl Strategy<Value = Vec<Uitem<Fvk>>> {
273        (
274            prop::option::of(
275                any::<u32>().prop_map(|h| Uitem::Metadata(MetadataItem::ExpiryHeight(h))),
276            ),
277            prop::option::of(
278                any::<u64>().prop_map(|t| Uitem::Metadata(MetadataItem::ExpiryTime(t))),
279            ),
280        )
281            .prop_map(|(h, t)| h.into_iter().chain(t).collect())
282    }
283
284    prop_compose! {
285        fn arb_r2_unified_fvk()(
286            shielded in arb_shielded_fvk(),
287            transparent in prop::option::of(arb_transparent_fvk()),
288            metadata in arb_metadata_items(),
289        ) -> Ufvk {
290            let mut items: Vec<Uitem<Fvk>> = transparent
291                .into_iter()
292                .chain(shielded)
293                .map(Uitem::Data)
294                .chain(metadata)
295                .collect();
296            items.sort_unstable_by(Uitem::encoding_order);
297            Ufvk {
298                revision: Revision::R2,
299                items,
300            }
301        }
302    }
303
304    // R2 allows transparent-only UFVKs.
305    prop_compose! {
306        fn arb_r2_transparent_only_fvk()(
307            transparent in arb_transparent_fvk(),
308            metadata in arb_metadata_items(),
309        ) -> Ufvk {
310            let mut items: Vec<Uitem<Fvk>> = core::iter::once(Uitem::Data(transparent))
311                .chain(metadata)
312                .collect();
313            items.sort_unstable_by(Uitem::encoding_order);
314            Ufvk {
315                revision: Revision::R2,
316                items,
317            }
318        }
319    }
320
321    proptest! {
322        #[test]
323        fn ufvk_roundtrip(
324            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
325            ufvk in arb_unified_fvk(),
326        ) {
327            let encoded = ufvk.encode(&network);
328            let decoded = Ufvk::decode(&encoded);
329            let decoded = decoded.map(|(net, _rev, ufvk)| (net, ufvk));
330            prop_assert_eq!(decoded, Ok((network, ufvk)));
331        }
332
333        #[test]
334        fn r2_ufvk_roundtrip(
335            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
336            ufvk in arb_r2_unified_fvk(),
337        ) {
338            let encoded = ufvk.encode(&network);
339            let decoded = Ufvk::decode(&encoded);
340            let decoded = decoded.map(|(net, _rev, ufvk)| (net, ufvk));
341            prop_assert_eq!(decoded, Ok((network, ufvk)));
342        }
343
344        #[test]
345        fn r2_transparent_only_ufvk_roundtrip(
346            network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
347            ufvk in arb_r2_transparent_only_fvk(),
348        ) {
349            let encoded = ufvk.encode(&network);
350            let decoded = Ufvk::decode(&encoded);
351            let decoded = decoded.map(|(net, _rev, ufvk)| (net, ufvk));
352            prop_assert_eq!(decoded, Ok((network, ufvk)));
353        }
354    }
355
356    #[test]
357    fn padding() {
358        // The test cases below use `Ufvk { revision: R0, items: vec![Uitem::Data(Fvk::Orchard([1; 96]))] }` as base.
359
360        // Invalid padding ([0xff; 16] instead of [b'u', 0x00, 0x00, 0x00...])
361        let invalid_padding = [
362            0x6b, 0x32, 0x44, 0xf1, 0xb, 0x67, 0xe9, 0x8f, 0x6, 0x57, 0xe3, 0x5, 0x17, 0xa0, 0x7,
363            0x5c, 0xb0, 0xc9, 0x23, 0xcc, 0xb7, 0x54, 0xac, 0x55, 0x6a, 0x65, 0x99, 0x95, 0x32,
364            0x97, 0xd5, 0x34, 0xa7, 0xc8, 0x6f, 0xc, 0xd7, 0x3b, 0xe0, 0x88, 0x19, 0xf3, 0x3e,
365            0x26, 0x19, 0xd6, 0x5f, 0x9a, 0x62, 0xc9, 0x6f, 0xad, 0x3b, 0xe5, 0xdd, 0xf1, 0xff,
366            0x5b, 0x4a, 0x13, 0x61, 0xc0, 0xd5, 0xa5, 0x87, 0xc5, 0x69, 0x48, 0xdb, 0x7e, 0xc6,
367            0x4e, 0xb0, 0x55, 0x41, 0x3f, 0xc0, 0x53, 0xbb, 0x79, 0x8b, 0x24, 0xa0, 0xfa, 0xd1,
368            0x6e, 0xea, 0x9, 0xea, 0xb3, 0xaf, 0x0, 0x7d, 0x86, 0x47, 0xdb, 0x8b, 0x38, 0xdd, 0x7b,
369            0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
370        ];
371        assert_eq!(
372            Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..], Revision::R0),
373            Err(ParseError::InvalidEncoding(
374                "Invalid padding bytes".to_owned()
375            ))
376        );
377
378        // Short padding (padded to 15 bytes instead of 16)
379        let truncated_padding = [
380            0xdf, 0xea, 0x84, 0x55, 0xc3, 0x4a, 0x7c, 0x6e, 0x9f, 0x83, 0x3, 0x21, 0x14, 0xb0,
381            0xcf, 0xb0, 0x60, 0x84, 0x75, 0x3a, 0xdc, 0xb9, 0x93, 0x16, 0xc0, 0x8f, 0x28, 0x5f,
382            0x61, 0x5e, 0xf0, 0x8e, 0x44, 0xae, 0xa6, 0x74, 0xc5, 0x64, 0xad, 0xfa, 0xdc, 0x7d,
383            0x64, 0x2a, 0x9, 0x47, 0x16, 0xf6, 0x5d, 0x8e, 0x46, 0xc4, 0xf0, 0x54, 0xfa, 0x5, 0x28,
384            0x1e, 0x3d, 0x7d, 0x37, 0xa5, 0x9f, 0x8b, 0x62, 0x78, 0xf6, 0x50, 0x18, 0x63, 0xe4,
385            0x51, 0x14, 0xae, 0x89, 0x41, 0x86, 0xd4, 0x9f, 0x10, 0x4b, 0x66, 0x2b, 0xf9, 0x46,
386            0x9c, 0xeb, 0xe8, 0x90, 0x8, 0xad, 0xd9, 0x6c, 0x6a, 0xf1, 0xed, 0xeb, 0x72, 0x44,
387            0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
388        ];
389        assert_eq!(
390            Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..], Revision::R0),
391            Err(ParseError::InvalidEncoding(
392                "Invalid padding bytes".to_owned()
393            ))
394        );
395    }
396
397    #[test]
398    fn truncated() {
399        // The test cases below start from an encoding of
400        //     `Ufvk { revision: R0, items: vec![Uitem::Data(Fvk::Orchard([1; 96])), Uitem::Data(Fvk::Sapling([2; 96]))] }`
401        // with the fvk data truncated, but valid padding.
402
403        // - Missing the last data byte of the Sapling fvk.
404        let truncated_sapling_data = vec![
405            0x43, 0xbf, 0x17, 0xa2, 0xb7, 0x85, 0xe7, 0x8e, 0xa4, 0x6d, 0x36, 0xa5, 0xf1, 0x1d,
406            0x74, 0xd1, 0x40, 0x6e, 0xed, 0xbd, 0x6b, 0x51, 0x6a, 0x36, 0x9c, 0xb3, 0x28, 0xd,
407            0x90, 0xa1, 0x1e, 0x3a, 0x67, 0xa2, 0x15, 0xc5, 0xfb, 0x82, 0x96, 0xf4, 0x35, 0x57,
408            0x71, 0x5d, 0xbb, 0xac, 0x30, 0x1d, 0x1, 0x6d, 0xdd, 0x2e, 0xf, 0x8, 0x4b, 0xcf, 0x5,
409            0xfe, 0x86, 0xd7, 0xa0, 0x9d, 0x94, 0x9f, 0x16, 0x5e, 0xa0, 0x3, 0x58, 0x81, 0x71,
410            0x40, 0xe4, 0xb8, 0xfc, 0x64, 0x75, 0x80, 0x46, 0x4f, 0x51, 0x2d, 0xb2, 0x51, 0xf,
411            0x22, 0x49, 0x53, 0x95, 0xbd, 0x7b, 0x66, 0xd9, 0x17, 0xda, 0x15, 0x62, 0xe0, 0xc6,
412            0xf8, 0x5c, 0xdf, 0x75, 0x6d, 0x7, 0xb, 0xf7, 0xab, 0xfc, 0x20, 0x61, 0xd0, 0xf4, 0x79,
413            0xfa, 0x4, 0xd3, 0xac, 0x8b, 0xf, 0x3c, 0x30, 0x23, 0x32, 0x37, 0x51, 0xc5, 0xfc, 0x66,
414            0x7e, 0xe1, 0x9c, 0xa8, 0xec, 0x52, 0x57, 0x7e, 0xc0, 0x31, 0x83, 0x1c, 0x31, 0x5,
415            0x1b, 0xc3, 0x70, 0xd3, 0x44, 0x74, 0xd2, 0x8a, 0xda, 0x32, 0x4, 0x93, 0xd2, 0xbf,
416            0xb4, 0xbb, 0xa, 0x9e, 0x8c, 0xe9, 0x8f, 0xe7, 0x8a, 0x95, 0xc8, 0x21, 0xfa, 0x12,
417            0x41, 0x2e, 0x69, 0x54, 0xf0, 0x7a, 0x9e, 0x20, 0x94, 0xa3, 0xaa, 0xc3, 0x50, 0x43,
418            0xc5, 0xe2, 0x32, 0x8b, 0x2e, 0x4f, 0xbb, 0xb4, 0xc0, 0x7f, 0x47, 0x35, 0xab, 0x89,
419            0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
420        ];
421        assert_matches!(
422            Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..], Revision::R0),
423            Err(ParseError::InvalidEncoding(_))
424        );
425
426        // - Truncated after the typecode of the Sapling fvk.
427        let truncated_after_sapling_typecode = [
428            0xac, 0x26, 0x5b, 0x19, 0x8f, 0x88, 0xb0, 0x7, 0xb3, 0x0, 0x91, 0x19, 0x52, 0xe1, 0x73,
429            0x48, 0xff, 0x66, 0x7a, 0xef, 0xcf, 0x57, 0x9c, 0x65, 0xe4, 0x6a, 0x7a, 0x1d, 0x19,
430            0x75, 0x6b, 0x43, 0xdd, 0xcf, 0xb9, 0x9a, 0xf3, 0x7a, 0xf8, 0xb, 0x23, 0x96, 0x64,
431            0x8c, 0x57, 0x56, 0x67, 0x9, 0x40, 0x35, 0xcb, 0xb1, 0xa4, 0x91, 0x4f, 0xdc, 0x39, 0x0,
432            0x98, 0x56, 0xa8, 0xf7, 0x25, 0x1a, 0xc8, 0xbc, 0xd7, 0xb3, 0xb0, 0xfa, 0x78, 0x6,
433            0xe8, 0x50, 0xfe, 0x92, 0xec, 0x5b, 0x1f, 0x74, 0xb9, 0xcf, 0x1f, 0x2e, 0x3b, 0x41,
434            0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
435        ];
436        assert_matches!(
437            Ufvk::parse_internal(
438                Ufvk::MAINNET,
439                &truncated_after_sapling_typecode[..],
440                Revision::R0
441            ),
442            Err(ParseError::InvalidEncoding(_))
443        );
444    }
445
446    #[test]
447    fn duplicate_typecode() {
448        let ufvk = Ufvk {
449            revision: Revision::R0,
450            items: vec![
451                Uitem::Data(Fvk::Sapling([1; 128])),
452                Uitem::Data(Fvk::Sapling([2; 128])),
453            ],
454        };
455        let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET);
456        assert_eq!(
457            Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..], Revision::R0),
458            Err(ParseError::DuplicateTypecode(Typecode::Data(
459                DataTypecode::Sapling
460            )))
461        );
462    }
463
464    #[test]
465    fn only_transparent() {
466        // Raw encoding of `Ufvk { items: vec![Uitem::Data(Fvk::P2pkh([0; 65]))] }`.
467        let encoded = [
468            0xc4, 0x70, 0xc8, 0x7a, 0xcc, 0xe6, 0x6b, 0x1a, 0x62, 0xc7, 0xcd, 0x5f, 0x76, 0xd8,
469            0xcc, 0x9c, 0x50, 0xbd, 0xce, 0x85, 0x80, 0xd7, 0x78, 0x25, 0x3e, 0x47, 0x9, 0x57,
470            0x7d, 0x6a, 0xdb, 0x10, 0xb4, 0x11, 0x80, 0x13, 0x4c, 0x83, 0x76, 0xb4, 0x6b, 0xbd,
471            0xef, 0x83, 0x5c, 0xa7, 0x68, 0xe6, 0xba, 0x41, 0x12, 0xbd, 0x43, 0x24, 0xf5, 0xaa,
472            0xa0, 0xf5, 0xf8, 0xe1, 0x59, 0xa0, 0x95, 0x85, 0x86, 0xf1, 0x9e, 0xcf, 0x8f, 0x94,
473            0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b,
474        ];
475
476        assert_eq!(
477            Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..], Revision::R0),
478            Err(ParseError::OnlyTransparent)
479        );
480    }
481
482    #[test]
483    fn fvks_are_sorted() {
484        let ufvk = Ufvk {
485            revision: Revision::R0,
486            items: vec![
487                Uitem::Data(Fvk::P2pkh([0; 65])),
488                Uitem::Data(Fvk::Orchard([0; 96])),
489                Uitem::Data(Fvk::Unknown {
490                    typecode: 0x50,
491                    data: vec![],
492                }),
493                Uitem::Data(Fvk::Sapling([0; 128])),
494            ],
495        };
496
497        assert_eq!(
498            ufvk.items(),
499            vec![
500                Fvk::Orchard([0; 96]),
501                Fvk::Sapling([0; 128]),
502                Fvk::P2pkh([0; 65]),
503                Fvk::Unknown {
504                    typecode: 0x50,
505                    data: vec![],
506                },
507            ]
508        )
509    }
510
511    #[test]
512    fn fvk_debug_redaction() {
513        assert_eq!(
514            format!("{:?}", Fvk::Orchard([0; 96])),
515            "Fvk::Orchard(\"...\")"
516        );
517        assert_eq!(
518            format!("{:?}", Fvk::Sapling([0; 128])),
519            "Fvk::Sapling(\"...\")"
520        );
521        assert_eq!(format!("{:?}", Fvk::P2pkh([0; 65])), "Fvk::P2pkh(\"...\")");
522        assert_eq!(
523            format!(
524                "{:?}",
525                Fvk::Unknown {
526                    typecode: 4242,
527                    data: vec![1, 2, 3],
528                }
529            ),
530            "Fvk::Unknown { typecode: 4242, data: \"...\" }"
531        );
532    }
533
534    #[test]
535    fn ufvk_debug_redaction() {
536        let ufvk = Ufvk {
537            revision: Revision::R0,
538            items: vec![
539                Uitem::Data(Fvk::P2pkh([0; 65])),
540                Uitem::Data(Fvk::Unknown {
541                    typecode: 7,
542                    data: vec![9, 9, 9],
543                }),
544            ],
545        };
546
547        assert_eq!(
548            format!("{ufvk:?}"),
549            "Ufvk { revision: R0, items: [Data(Fvk::P2pkh(\"...\")), Data(Fvk::Unknown { typecode: 7, data: \"...\" })] }"
550        );
551    }
552
553    #[test]
554    fn r2_transparent_only_ufvk() {
555        // R2 UVKs allow transparent-only.
556        let items = vec![Uitem::Data(Fvk::P2pkh([1; 65]))];
557        let ufvk = Ufvk::try_from_items(Revision::R2, items).unwrap();
558        assert_eq!(ufvk.revision(), Revision::R2);
559
560        // Round-trip.
561        let encoded = ufvk.encode(&NetworkType::Main);
562        assert!(encoded.starts_with("uvf"));
563        let (net, rev, decoded) = Ufvk::decode(&encoded).unwrap();
564        assert_eq!(net, NetworkType::Main);
565        assert_eq!(rev, Revision::R2);
566        assert_eq!(decoded, ufvk);
567    }
568
569    #[test]
570    fn r2_ufvk_with_expiry() {
571        let items = vec![
572            Uitem::Data(Fvk::Orchard([2; 96])),
573            Uitem::Metadata(MetadataItem::ExpiryHeight(500_000)),
574            Uitem::Metadata(MetadataItem::ExpiryTime(1_700_000_000)),
575        ];
576        let ufvk = Ufvk::try_from_items(Revision::R2, items).unwrap();
577
578        let encoded = ufvk.encode(&NetworkType::Test);
579        let (net, rev, decoded) = Ufvk::decode(&encoded).unwrap();
580        assert_eq!(net, NetworkType::Test);
581        assert_eq!(rev, Revision::R2);
582        assert_eq!(decoded, ufvk);
583
584        let meta = decoded.metadata_items();
585        assert_eq!(meta.len(), 2);
586        assert_eq!(*meta[0], MetadataItem::ExpiryHeight(500_000));
587        assert_eq!(*meta[1], MetadataItem::ExpiryTime(1_700_000_000));
588    }
589
590    /// Builds a P2SH viewing key item payload from a descriptor template and a number of
591    /// zeroed key information entries.
592    fn p2sh_item_payload(template: &str, n_keys: usize) -> Vec<u8> {
593        /// Chain code plus compressed pubkey.
594        const KEY_INFO_LEN: usize = 65;
595        let mut payload = vec![];
596        zcash_encoding::CompactSize::write(&mut payload, template.len()).unwrap();
597        payload.extend_from_slice(template.as_bytes());
598        zcash_encoding::CompactSize::write(&mut payload, n_keys).unwrap();
599        payload.extend_from_slice(&vec![0u8; n_keys * KEY_INFO_LEN]);
600        payload
601    }
602
603    #[test]
604    fn r0_construction_rejects_p2sh_item() {
605        // Revision 0 assigns no meaning to a P2SH viewing key item.
606        let p2sh = p2sh_item_payload("sh(sortedmulti(2,@0/**,@1/**,@2/**))", 3);
607        let items = vec![
608            Uitem::Data(Fvk::Sapling([0; 128])),
609            Uitem::Data(Fvk::P2sh(p2sh)),
610        ];
611        assert_eq!(
612            Ufvk::try_from_items(Revision::R0, items.clone()),
613            Err(ParseError::NotDefinedInRevision {
614                typecode: Typecode::P2SH,
615                revision: Revision::R0,
616            })
617        );
618        assert!(Ufvk::try_from_items(Revision::R2, items).is_ok());
619    }
620
621    #[test]
622    fn p2sh_fvk_item_parsing() {
623        use crate::kind::unified::{P2shItemError, ParseError, private::SealedItem};
624
625        let valid = p2sh_item_payload("sh(sortedmulti(2,@0/**,@1/**,@2/**))", 3);
626        assert_eq!(
627            Fvk::parse(DataTypecode::P2sh, &valid),
628            Ok(Fvk::P2sh(valid.clone()))
629        );
630
631        // A UFVK template must use `/**` multipath notation, not `/*`.
632        let ivk_notation = p2sh_item_payload("sh(sortedmulti(2,@0/*,@1/*,@2/*))", 3);
633        assert_eq!(
634            Fvk::parse(DataTypecode::P2sh, &ivk_notation),
635            Err(ParseError::InvalidP2shItem(P2shItemError::Multipath))
636        );
637
638        // The number of placeholders must equal the number of key entries.
639        let missing_key = p2sh_item_payload("sh(sortedmulti(2,@0/**,@1/**))", 3);
640        assert_eq!(
641            Fvk::parse(DataTypecode::P2sh, &missing_key),
642            Err(ParseError::InvalidP2shItem(
643                P2shItemError::PlaceholderCount {
644                    placeholders: 2,
645                    keys: 3,
646                }
647            ))
648        );
649
650        // Each key entry must be referenced exactly once.
651        let duplicate_placeholder = p2sh_item_payload("sh(sortedmulti(2,@0/**,@0/**))", 2);
652        assert_eq!(
653            Fvk::parse(DataTypecode::P2sh, &duplicate_placeholder),
654            Err(ParseError::InvalidP2shItem(
655                P2shItemError::PlaceholderCount {
656                    placeholders: 2,
657                    keys: 2,
658                }
659            ))
660        );
661
662        // Trailing bytes after the key information vector are rejected.
663        let mut trailing = valid.clone();
664        trailing.push(0);
665        assert_eq!(
666            Fvk::parse(DataTypecode::P2sh, &trailing),
667            Err(ParseError::InvalidP2shItem(P2shItemError::Malformed))
668        );
669
670        // The descriptor template must be US-ASCII.
671        let mut non_ascii = vec![];
672        zcash_encoding::CompactSize::write(&mut non_ascii, 1usize).unwrap();
673        non_ascii.push(0xFF);
674        zcash_encoding::CompactSize::write(&mut non_ascii, 0usize).unwrap();
675        assert_eq!(
676            Fvk::parse(DataTypecode::P2sh, &non_ascii),
677            Err(ParseError::InvalidP2shItem(P2shItemError::TemplateEncoding))
678        );
679    }
680
681    #[test]
682    fn r0_ufvk_treats_p2sh_item_as_unrecognised() {
683        // ZIP 316 gives Typecode 0x01 no meaning in a Revision 0 UFVK, so a consumer
684        // must retain it as an unrecognised item rather than interpret it as a P2SH
685        // viewing key item. This holds whether or not the payload happens to be a
686        // well-formed BIP 388 wallet policy.
687        const P2SH_TYPECODE: u32 = 0x01;
688
689        for payload in [
690            p2sh_item_payload("sh(sortedmulti(2,@0/**,@1/**,@2/**))", 3),
691            b"not a wallet policy".to_vec(),
692        ] {
693            let ufvk = Ufvk::try_from_items(
694                Revision::R0,
695                vec![
696                    Uitem::Data(Fvk::Unknown {
697                        typecode: P2SH_TYPECODE,
698                        data: payload.clone(),
699                    }),
700                    Uitem::Data(Fvk::Sapling([0; 128])),
701                ],
702            )
703            .unwrap();
704
705            let (_, revision, decoded) = Ufvk::decode(&ufvk.encode(&NetworkType::Main)).unwrap();
706            assert_eq!(revision, Revision::R0);
707            assert!(decoded.items().contains(&Fvk::Unknown {
708                typecode: P2SH_TYPECODE,
709                data: payload,
710            }));
711            assert!(
712                !decoded
713                    .items()
714                    .iter()
715                    .any(|fvk| matches!(fvk, Fvk::P2sh(_)))
716            );
717        }
718    }
719}