Skip to main content

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