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