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