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 Ivk {
13 Orchard([u8; 64]),
17
18 Sapling([u8; 64]),
26
27 P2pkh([u8; 65]),
42
43 P2sh(Vec<u8>),
53
54 Unknown {
55 typecode: u32,
56 data: Vec<u8>,
57 },
58}
59
60impl fmt::Debug for Ivk {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Ivk::Orchard(_) => f.debug_tuple("Ivk::Orchard").field(&"...").finish(),
64 Ivk::Sapling(_) => f.debug_tuple("Ivk::Sapling").field(&"...").finish(),
65 Ivk::P2pkh(_) => f.debug_tuple("Ivk::P2pkh").field(&"...").finish(),
66 Ivk::P2sh(_) => f.debug_tuple("Ivk::P2sh").field(&"...").finish(),
67 Ivk::Unknown { typecode, .. } => f
68 .debug_struct("Ivk::Unknown")
69 .field("typecode", typecode)
70 .field("data", &"...")
71 .finish(),
72 }
73 }
74}
75
76impl SealedItem for Ivk {
77 fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
78 if typecode == DataTypecode::P2sh {
79 validate_p2sh_item(P2shItemKind::IncomingViewing, data)
80 .map_err(ParseError::InvalidP2shItem)?;
81 return Ok(Ivk::P2sh(data.to_vec()));
82 }
83 let data = data.to_vec();
84 match typecode {
85 DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh),
86 DataTypecode::P2sh => unreachable!("handled above"),
87 DataTypecode::Sapling => data.try_into().map(Ivk::Sapling),
88 DataTypecode::Orchard => data.try_into().map(Ivk::Orchard),
89 DataTypecode::Unknown(tc) => Ok(Ivk::Unknown { typecode: tc, data }),
90 }
91 .map_err(|e| {
92 ParseError::InvalidEncoding(format!(
93 "Invalid ivk for typecode {}: {e:?}",
94 u32::from(typecode)
95 ))
96 })
97 }
98
99 fn typecode(&self) -> DataTypecode {
100 match self {
101 Ivk::P2pkh(_) => DataTypecode::P2pkh,
102 Ivk::P2sh(_) => DataTypecode::P2sh,
103 Ivk::Sapling(_) => DataTypecode::Sapling,
104 Ivk::Orchard(_) => DataTypecode::Orchard,
105 Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
106 }
107 }
108
109 fn data(&self) -> &[u8] {
110 match self {
111 Ivk::P2pkh(data) => data,
112 Ivk::P2sh(data) => data,
113 Ivk::Sapling(data) => data,
114 Ivk::Orchard(data) => data,
115 Ivk::Unknown { data, .. } => data,
116 }
117 }
118}
119
120#[derive(Clone, Debug, PartialEq, Eq, Hash)]
151pub struct Uivk {
152 pub(crate) revision: Revision,
153 pub(crate) items: Vec<Uitem<Ivk>>,
154}
155
156impl Container for Uivk {
157 type Item = Ivk;
158
159 fn revision(&self) -> Revision {
160 self.revision
161 }
162
163 fn items_as_parsed(&self) -> &[Uitem<Ivk>] {
168 &self.items
169 }
170}
171
172impl Encoding for Uivk {}
173
174impl SealedContainer for Uivk {
175 const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_IVK;
176 const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_IVK;
177 const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK;
178
179 const MAINNET_R2: &'static str = constants::mainnet::HRP_UNIFIED_IVK_R2;
180 const TESTNET_R2: &'static str = constants::testnet::HRP_UNIFIED_IVK_R2;
181 const REGTEST_R2: &'static str = constants::regtest::HRP_UNIFIED_IVK_R2;
182
183 const MAINNET_R2_TI: &'static str = constants::mainnet::HRP_UNIFIED_IVK_R2;
184 const TESTNET_R2_TI: &'static str = constants::testnet::HRP_UNIFIED_IVK_R2;
185 const REGTEST_R2_TI: &'static str = constants::regtest::HRP_UNIFIED_IVK_R2;
186
187 const IS_ADDRESS: bool = false;
188
189 fn from_inner(revision: Revision, items: Vec<Uitem<Ivk>>) -> Self {
190 Self { revision, items }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use alloc::borrow::ToOwned;
197 use alloc::vec::Vec;
198
199 use assert_matches::assert_matches;
200
201 use proptest::{
202 array::{uniform1, uniform32},
203 prelude::*,
204 sample::select,
205 };
206
207 use super::{Ivk, ParseError, Uivk};
208 use crate::kind::unified::{
209 Container, DataTypecode, Encoding, Revision, Typecode, Uitem, private::SealedContainer,
210 };
211 use zcash_protocol::consensus::NetworkType;
212
213 prop_compose! {
214 fn uniform64()(a in uniform32(0u8..), b in uniform32(0u8..)) -> [u8; 64] {
215 let mut c = [0; 64];
216 c[..32].copy_from_slice(&a);
217 c[32..].copy_from_slice(&b);
218 c
219 }
220 }
221
222 prop_compose! {
223 fn uniform65()(a in uniform1(0u8..), b in uniform64()) -> [u8; 65] {
224 let mut c = [0; 65];
225 c[..1].copy_from_slice(&a);
226 c[1..].copy_from_slice(&b);
227 c
228 }
229 }
230
231 fn arb_shielded_ivk() -> impl Strategy<Value = Vec<Ivk>> {
232 prop_oneof![
233 vec![uniform64().prop_map(Ivk::Sapling)],
234 vec![uniform64().prop_map(Ivk::Orchard)],
235 vec![
236 uniform64().prop_map(Ivk::Sapling as fn([u8; 64]) -> Ivk),
237 uniform64().prop_map(Ivk::Orchard)
238 ],
239 ]
240 }
241
242 fn arb_transparent_ivk() -> impl Strategy<Value = Ivk> {
243 uniform65().prop_map(Ivk::P2pkh)
244 }
245
246 prop_compose! {
247 fn arb_unified_ivk()(
248 shielded in arb_shielded_ivk(),
249 transparent in prop::option::of(arb_transparent_ivk()),
250 ) -> Uivk {
251 let mut items: Vec<Uitem<Ivk>> = transparent
252 .into_iter()
253 .chain(shielded)
254 .map(Uitem::Data)
255 .collect();
256 items.sort_unstable_by(Uitem::encoding_order);
257 Uivk {
258 revision: Revision::R0,
259 items,
260 }
261 }
262 }
263
264 use crate::kind::unified::MetadataItem;
265
266 fn arb_metadata_items() -> impl Strategy<Value = Vec<Uitem<Ivk>>> {
267 (
268 prop::option::of(
269 any::<u32>().prop_map(|h| Uitem::Metadata(MetadataItem::ExpiryHeight(h))),
270 ),
271 prop::option::of(
272 any::<u64>().prop_map(|t| Uitem::Metadata(MetadataItem::ExpiryTime(t))),
273 ),
274 )
275 .prop_map(|(h, t)| h.into_iter().chain(t).collect())
276 }
277
278 prop_compose! {
279 fn arb_r2_unified_ivk()(
280 shielded in arb_shielded_ivk(),
281 transparent in prop::option::of(arb_transparent_ivk()),
282 metadata in arb_metadata_items(),
283 ) -> Uivk {
284 let mut items: Vec<Uitem<Ivk>> = transparent
285 .into_iter()
286 .chain(shielded)
287 .map(Uitem::Data)
288 .chain(metadata)
289 .collect();
290 items.sort_unstable_by(Uitem::encoding_order);
291 Uivk {
292 revision: Revision::R2,
293 items,
294 }
295 }
296 }
297
298 prop_compose! {
300 fn arb_r2_transparent_only_ivk()(
301 transparent in arb_transparent_ivk(),
302 metadata in arb_metadata_items(),
303 ) -> Uivk {
304 let mut items: Vec<Uitem<Ivk>> = core::iter::once(Uitem::Data(transparent))
305 .chain(metadata)
306 .collect();
307 items.sort_unstable_by(Uitem::encoding_order);
308 Uivk {
309 revision: Revision::R2,
310 items,
311 }
312 }
313 }
314
315 proptest! {
316 #[test]
317 fn uivk_roundtrip(
318 network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
319 uivk in arb_unified_ivk(),
320 ) {
321 let encoded = uivk.encode(&network);
322 let decoded = Uivk::decode(&encoded);
323 let decoded = decoded.map(|(net, _rev, uivk)| (net, uivk));
324 prop_assert_eq!(decoded, Ok((network, uivk)));
325 }
326
327 #[test]
328 fn r2_uivk_roundtrip(
329 network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
330 uivk in arb_r2_unified_ivk(),
331 ) {
332 let encoded = uivk.encode(&network);
333 let decoded = Uivk::decode(&encoded);
334 let decoded = decoded.map(|(net, _rev, uivk)| (net, uivk));
335 prop_assert_eq!(decoded, Ok((network, uivk)));
336 }
337
338 #[test]
339 fn r2_transparent_only_uivk_roundtrip(
340 network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]),
341 uivk in arb_r2_transparent_only_ivk(),
342 ) {
343 let encoded = uivk.encode(&network);
344 let decoded = Uivk::decode(&encoded);
345 let decoded = decoded.map(|(net, _rev, uivk)| (net, uivk));
346 prop_assert_eq!(decoded, Ok((network, uivk)));
347 }
348 }
349
350 #[test]
351 fn padding() {
352 let invalid_padding = [
354 0xba, 0xbc, 0xc0, 0x71, 0xcd, 0x3b, 0xfd, 0x9a, 0x32, 0x19, 0x7e, 0xeb, 0x8a, 0xa7,
355 0x6e, 0xd4, 0xac, 0xcb, 0x59, 0xc2, 0x54, 0x26, 0xc6, 0xab, 0x71, 0xc7, 0xc3, 0x72,
356 0xc, 0xa9, 0xad, 0xa4, 0xad, 0x8c, 0x9e, 0x35, 0x7b, 0x4c, 0x5d, 0xc7, 0x66, 0x12,
357 0x8a, 0xc5, 0x42, 0x89, 0xc1, 0x77, 0x32, 0xdc, 0xe8, 0x4b, 0x51, 0x31, 0x30, 0x3,
358 0x20, 0xe3, 0xb6, 0x8c, 0xbb, 0xab, 0xe8, 0x89, 0xf8, 0xed, 0xac, 0x6d, 0x8e, 0xb1,
359 0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
360 ];
361 assert_eq!(
362 Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..], Revision::R0),
363 Err(ParseError::InvalidEncoding(
364 "Invalid padding bytes".to_owned()
365 ))
366 );
367
368 let truncated_padding = [
370 0x96, 0x73, 0x6a, 0x56, 0xbc, 0x44, 0x38, 0xe2, 0x47, 0x41, 0x1c, 0x70, 0xe4, 0x6,
371 0x87, 0xbe, 0xb6, 0x90, 0xbd, 0xab, 0x1b, 0xd8, 0x27, 0x10, 0x0, 0x21, 0x30, 0x2, 0x77,
372 0x87, 0x0, 0x25, 0x96, 0x94, 0x8f, 0x1e, 0x39, 0xd2, 0xd8, 0x65, 0xb4, 0x3c, 0x72,
373 0xd8, 0xac, 0xec, 0x5b, 0xa2, 0x18, 0x62, 0x3f, 0xb, 0x88, 0xb4, 0x41, 0xf1, 0x55,
374 0x39, 0x53, 0xbf, 0x2a, 0xd6, 0xcf, 0xdd, 0x46, 0xb7, 0xd8, 0xc1, 0x39, 0x34, 0x4d,
375 0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
376 ];
377 assert_eq!(
378 Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..], Revision::R0),
379 Err(ParseError::InvalidEncoding(
380 "Invalid padding bytes".to_owned()
381 ))
382 );
383 }
384
385 #[test]
386 fn truncated() {
387 let truncated_sapling_data = [
389 0xce, 0xbc, 0xfe, 0xc5, 0xef, 0x2d, 0xe, 0x66, 0xc2, 0x8c, 0x34, 0xdc, 0x2e, 0x24,
390 0xd2, 0xc7, 0x4b, 0xac, 0x36, 0xe0, 0x43, 0x72, 0xa7, 0x33, 0xa4, 0xe, 0xe0, 0x52,
391 0x15, 0x64, 0x66, 0x92, 0x36, 0xa7, 0x60, 0x8e, 0x48, 0xe8, 0xb0, 0x30, 0x4d, 0xcb,
392 0xd, 0x6f, 0x5, 0xd4, 0xb8, 0x72, 0x6a, 0xdc, 0x6c, 0x5c, 0xa, 0xf8, 0xdf, 0x95, 0x5a,
393 0xba, 0xe1, 0xaa, 0x82, 0x51, 0xe2, 0x70, 0x8d, 0x13, 0x16, 0x88, 0x6a, 0xc0, 0xc1,
394 0x99, 0x3c, 0xaf, 0x2c, 0x16, 0x54, 0x80, 0x7e, 0xb, 0xad, 0x31, 0x29, 0x26, 0xdd,
395 0x7a, 0x55, 0x98, 0x1, 0x18, 0xb, 0x14, 0x94, 0xb2, 0x6b, 0x81, 0x67, 0x73, 0xa6, 0xd0,
396 0x20, 0x94, 0x17, 0x3a, 0xf9, 0x98, 0x43, 0x58, 0xd6, 0x1, 0x10, 0x73, 0x32, 0xb4,
397 0x99, 0xad, 0x6b, 0xfe, 0xc0, 0x97, 0xaf, 0xd2, 0xee, 0x8, 0xe5, 0x83, 0x6b, 0xb6,
398 0xd9, 0x0, 0xef, 0x84, 0xff, 0xe8, 0x58, 0xba, 0xe8, 0x10, 0xea, 0x2d, 0xee, 0x72,
399 0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
400 ];
401 assert_matches!(
402 Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..], Revision::R0),
403 Err(ParseError::InvalidEncoding(_))
404 );
405
406 let truncated_after_sapling_typecode = [
408 0xf7, 0x3, 0xd8, 0xbe, 0x6a, 0x27, 0xfa, 0xa1, 0xd3, 0x11, 0xea, 0x25, 0x94, 0xe2, 0xb,
409 0xde, 0xed, 0x6a, 0xaa, 0x8, 0x46, 0x7d, 0xe4, 0xb1, 0xe, 0xf1, 0xde, 0x61, 0xd7, 0x95,
410 0xf7, 0x82, 0x62, 0x32, 0x7a, 0x73, 0x8c, 0x55, 0x93, 0xa1, 0x63, 0x75, 0xe2, 0xca,
411 0xcb, 0x73, 0xd5, 0xe5, 0xa3, 0xbd, 0xb3, 0xf2, 0x26, 0xfa, 0x1c, 0xa2, 0xad, 0xb6,
412 0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
413 ];
414 assert_matches!(
415 Uivk::parse_internal(
416 Uivk::MAINNET,
417 &truncated_after_sapling_typecode[..],
418 Revision::R0
419 ),
420 Err(ParseError::InvalidEncoding(_))
421 );
422 }
423
424 #[test]
425 fn duplicate_typecode() {
426 let uivk = Uivk {
427 revision: Revision::R0,
428 items: vec![
429 Uitem::Data(Ivk::Sapling([1; 64])),
430 Uitem::Data(Ivk::Sapling([2; 64])),
431 ],
432 };
433 let encoded = uivk.encode(&NetworkType::Main);
434 assert_eq!(
435 Uivk::decode(&encoded).map(|(net, _rev, uivk)| (net, uivk)),
436 Err(ParseError::DuplicateTypecode(Typecode::Data(
437 DataTypecode::Sapling
438 )))
439 );
440 }
441
442 #[test]
443 fn only_transparent() {
444 let encoded = [
446 0x12, 0x51, 0x37, 0xc7, 0xac, 0x8c, 0xd, 0x13, 0x3a, 0x5f, 0xc6, 0x84, 0x53, 0x90,
447 0xf8, 0xe7, 0x23, 0x34, 0xfb, 0xda, 0x49, 0x3c, 0x87, 0x1c, 0x8f, 0x1a, 0xe1, 0x63,
448 0xba, 0xdf, 0x77, 0x64, 0x43, 0xcf, 0xdc, 0x37, 0x1f, 0xd2, 0x89, 0x60, 0xe3, 0x77,
449 0x20, 0xd0, 0x1c, 0x5, 0x40, 0xe5, 0x43, 0x55, 0xc4, 0xe5, 0xf8, 0xaa, 0xe, 0x7a, 0xe7,
450 0x8c, 0x53, 0x15, 0xb8, 0x8f, 0x90, 0x14, 0x33, 0x30, 0x52, 0x2b, 0x8, 0x89, 0x90,
451 0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64,
452 ];
453
454 assert_eq!(
455 Uivk::parse_internal(Uivk::MAINNET, &encoded[..], Revision::R0),
456 Err(ParseError::OnlyTransparent)
457 );
458 }
459
460 #[test]
461 fn ivks_are_sorted() {
462 let uivk = Uivk {
463 revision: Revision::R0,
464 items: vec![
465 Uitem::Data(Ivk::P2pkh([0; 65])),
466 Uitem::Data(Ivk::Orchard([0; 64])),
467 Uitem::Data(Ivk::Unknown {
468 typecode: 0x50,
469 data: vec![],
470 }),
471 Uitem::Data(Ivk::Sapling([0; 64])),
472 ],
473 };
474
475 assert_eq!(
476 uivk.items(),
477 vec![
478 Ivk::Orchard([0; 64]),
479 Ivk::Sapling([0; 64]),
480 Ivk::P2pkh([0; 65]),
481 Ivk::Unknown {
482 typecode: 0x50,
483 data: vec![],
484 },
485 ]
486 )
487 }
488
489 #[test]
490 fn ivk_debug_redaction() {
491 assert_eq!(
492 format!("{:?}", Ivk::Orchard([0; 64])),
493 "Ivk::Orchard(\"...\")"
494 );
495 assert_eq!(
496 format!("{:?}", Ivk::Sapling([0; 64])),
497 "Ivk::Sapling(\"...\")"
498 );
499 assert_eq!(format!("{:?}", Ivk::P2pkh([0; 65])), "Ivk::P2pkh(\"...\")");
500 assert_eq!(
501 format!(
502 "{:?}",
503 Ivk::Unknown {
504 typecode: 31337,
505 data: vec![4, 5, 6],
506 }
507 ),
508 "Ivk::Unknown { typecode: 31337, data: \"...\" }"
509 );
510 }
511
512 #[test]
513 fn uivk_debug_redaction() {
514 let uivk = Uivk {
515 revision: Revision::R0,
516 items: vec![
517 Uitem::Data(Ivk::Sapling([0; 64])),
518 Uitem::Data(Ivk::Unknown {
519 typecode: 9,
520 data: vec![8, 8, 8],
521 }),
522 ],
523 };
524
525 assert_eq!(
526 format!("{uivk:?}"),
527 "Uivk { revision: R0, items: [Data(Ivk::Sapling(\"...\")), Data(Ivk::Unknown { typecode: 9, data: \"...\" })] }"
528 );
529 }
530
531 #[test]
532 fn p2sh_ivk_item_parsing() {
533 use crate::kind::unified::{P2shItemError, ParseError, private::SealedItem};
534
535 const KEY_INFO_LEN: usize = 65;
537 let payload = |template: &str, n_keys: usize| {
538 let mut payload = vec![];
539 zcash_encoding::CompactSize::write(&mut payload, template.len()).unwrap();
540 payload.extend_from_slice(template.as_bytes());
541 zcash_encoding::CompactSize::write(&mut payload, n_keys).unwrap();
542 payload.extend_from_slice(&vec![0u8; n_keys * KEY_INFO_LEN]);
543 payload
544 };
545
546 let valid = payload("sh(sortedmulti(2,@0/*,@1/*,@2/*))", 3);
547 assert_eq!(
548 Ivk::parse(DataTypecode::P2sh, &valid),
549 Ok(Ivk::P2sh(valid.clone()))
550 );
551
552 let fvk_notation = payload("sh(sortedmulti(2,@0/**,@1/**,@2/**))", 3);
554 assert_eq!(
555 Ivk::parse(DataTypecode::P2sh, &fvk_notation),
556 Err(ParseError::InvalidP2shItem(P2shItemError::Multipath))
557 );
558 }
559
560 #[test]
561 fn r0_uivk_treats_p2sh_item_as_unrecognised() {
562 const P2SH_TYPECODE: u32 = 0x01;
567 const KEY_INFO_LEN: usize = 65;
569
570 let policy = |template: &str, n_keys: usize| {
571 let mut payload = vec![];
572 zcash_encoding::CompactSize::write(&mut payload, template.len()).unwrap();
573 payload.extend_from_slice(template.as_bytes());
574 zcash_encoding::CompactSize::write(&mut payload, n_keys).unwrap();
575 payload.extend_from_slice(&vec![0u8; n_keys * KEY_INFO_LEN]);
576 payload
577 };
578
579 for payload in [
580 policy("sh(sortedmulti(2,@0/*,@1/*,@2/*))", 3),
581 b"not a wallet policy".to_vec(),
582 ] {
583 let uivk = Uivk::try_from_items(
584 Revision::R0,
585 vec![
586 Uitem::Data(Ivk::Unknown {
587 typecode: P2SH_TYPECODE,
588 data: payload.clone(),
589 }),
590 Uitem::Data(Ivk::Sapling([0; 64])),
591 ],
592 )
593 .unwrap();
594
595 let (_, revision, decoded) = Uivk::decode(&uivk.encode(&NetworkType::Main)).unwrap();
596 assert_eq!(revision, Revision::R0);
597 assert!(decoded.items().contains(&Ivk::Unknown {
598 typecode: P2SH_TYPECODE,
599 data: payload,
600 }));
601 assert!(
602 !decoded
603 .items()
604 .iter()
605 .any(|ivk| matches!(ivk, Ivk::P2sh(_)))
606 );
607 }
608 }
609}