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#[derive(Clone, PartialEq, Eq, Hash)]
13pub enum Fvk {
14 Orchard([u8; 96]),
18
19 Sapling([u8; 128]),
23
24 P2pkh([u8; 65]),
38
39 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#[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 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 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 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 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 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 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 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 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 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 fn p2sh_item_payload(template: &str, n_keys: usize) -> Vec<u8> {
594 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 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 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 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 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 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 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}