Skip to main content

zcash_address/kind/unified/
fvk.rs

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