diff --git a/libs/ur-parse-lib/src/keystone_ur_encoder.rs b/libs/ur-parse-lib/src/keystone_ur_encoder.rs index d66ef8f..4fdf857 100644 --- a/libs/ur-parse-lib/src/keystone_ur_encoder.rs +++ b/libs/ur-parse-lib/src/keystone_ur_encoder.rs @@ -99,7 +99,7 @@ impl KeystoneUREncoder { #[cfg(test)] mod tests { - use crate::keystone_ur_decoder::{probe_decode, MultiURParseResult, URParseResult}; + use crate::keystone_ur_decoder::{MultiURParseResult, URParseResult, probe_decode}; use crate::keystone_ur_encoder::{cyclic_encode, probe_encode}; use alloc::string::ToString; use alloc::vec; @@ -108,65 +108,32 @@ mod tests { use ur_registry::crypto_psbt::CryptoPSBT; use ur_registry::extend::qr_hardware_call::QRHardwareCall; use ur_registry::traits::{RegistryItem, UR}; - use ur_registry::zcash::zcash_sign_batch::{ - ZcashSignBatch, ZcashSignMessage, ZCASH_SIGN_BATCH_NETWORK_MAINNET, - ZCASH_SIGN_BATCH_VERSION, ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - }; + use ur_registry::zcash::zcash_batch_sig_result::ZcashBatchSigResult; + use ur_registry::zcash::zcash_sign_batch::ZcashSignBatch; use ur_registry::zcash::zcash_sign_result::{ - ZcashSignMessageResult, ZcashSignResult, ZCASH_SIGN_RESULT_KIND_PCZT_V1, - ZCASH_SIGN_RESULT_VERSION, ZCASH_SIGN_STATUS_SIGNED, + ZCASH_SIGN_RESULT_KIND_PCZT_V1, ZCASH_SIGN_RESULT_VERSION, ZCASH_SIGN_STATUS_SIGNED, + ZcashSignMessageResult, ZcashSignResult, }; #[test] fn test_encode_decode_zcash_sign_batch_ur() { - let payload_digest = - Vec::from_hex("7a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da4") - .unwrap(); - let batch = ZcashSignBatch::new( - ZCASH_SIGN_BATCH_VERSION, - vec![0xaa, 0xbb], - ZCASH_SIGN_BATCH_NETWORK_MAINNET, - vec![ZcashSignMessage::new( - vec![0x01], - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - b"pczt-request".to_vec(), - Some(payload_digest.clone()), - )], - Some(false), - ); + // Serialization of an empty PCZT BatchSignRequest: + // "PCZB" || batch_version_le || pczt_version_le || Postcard body. + let request = vec![b'P', b'C', b'Z', b'B', 1, 0, 0, 0, 2, 0, 0, 0, 0]; + let request_id = vec![0xaa, 0xbb]; + let batch = ZcashSignBatch::new(request_id.clone(), request.clone()); let cbor: Vec = batch.try_into().unwrap(); let encoded = probe_encode(&cbor, 400, ZcashSignBatch::get_registry_type().get_type()).unwrap(); - let literal_ur = "ur:zcash-sign-batch/onadadaofwpkrkaxadaalyoxadfpadaoadaxgsjoiaknjydpjpihjskpihjkjyamhdcxkniyvarnayknzevtiygyhslfluttcasevoadzcrosatsdilnwyfdhydtldkelgoxbdwkeccarlis"; assert!(!encoded.is_multi_part); - assert_eq!(encoded.data, literal_ur); - let decoded: URParseResult = probe_decode(literal_ur.to_string()).unwrap(); + let decoded: URParseResult = probe_decode(encoded.data).unwrap(); let decoded_batch = decoded.data.unwrap(); assert_eq!(decoded.ur_type.unwrap().get_type_str(), "zcash-sign-batch"); - assert_eq!(decoded_batch.get_version(), ZCASH_SIGN_BATCH_VERSION); - assert_eq!(decoded_batch.get_request_id(), &vec![0xaa, 0xbb]); - assert_eq!( - decoded_batch.get_network(), - ZCASH_SIGN_BATCH_NETWORK_MAINNET - ); - assert!(!decoded_batch.get_atomic()); - assert_eq!(decoded_batch.get_messages().len(), 1); - assert_eq!(decoded_batch.get_messages()[0].get_id(), &vec![0x01]); - assert_eq!( - decoded_batch.get_messages()[0].get_kind(), - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 - ); - assert_eq!( - decoded_batch.get_messages()[0].get_payload(), - &b"pczt-request".to_vec() - ); - assert_eq!( - decoded_batch.get_messages()[0].get_payload_digest(), - Some(&payload_digest) - ); + assert_eq!(decoded_batch.get_data(), request); + assert_eq!(decoded_batch.get_request_id(), request_id); } #[test] @@ -229,24 +196,39 @@ mod tests { ); } + #[test] + fn test_encode_decode_zcash_batch_sig_result_ur() { + // Serialization of an empty PCZT BatchSignResponse: + // "PCZS" || batch_version_le || Postcard body. + let response = vec![b'P', b'C', b'Z', b'S', 1, 0, 0, 0, 0]; + let request_id = vec![0xaa, 0xbb]; + let result = ZcashBatchSigResult::new(request_id.clone(), response.clone()); + let cbor: Vec = result.try_into().unwrap(); + let encoded = probe_encode( + &cbor, + 400, + ZcashBatchSigResult::get_registry_type().get_type(), + ) + .unwrap(); + + assert!(!encoded.is_multi_part); + + let decoded: URParseResult = probe_decode(encoded.data).unwrap(); + let decoded_result = decoded.data.unwrap(); + + assert_eq!( + decoded.ur_type.unwrap().get_type_str(), + "zcash-batch-sig-result" + ); + assert_eq!(decoded_result.get_data(), response); + assert_eq!(decoded_result.get_request_id(), request_id); + } + #[test] fn test_encode_decode_multipart_zcash_sign_batch_ur() { let payload = vec![0x42; 1024]; - let payload_digest = - Vec::from_hex("7a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da4") - .unwrap(); - let batch = ZcashSignBatch::new( - ZCASH_SIGN_BATCH_VERSION, - vec![0xaa, 0xbb], - ZCASH_SIGN_BATCH_NETWORK_MAINNET, - vec![ZcashSignMessage::new( - vec![0x01], - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - payload.clone(), - Some(payload_digest.clone()), - )], - Some(true), - ); + let request_id = vec![0xaa, 0xbb]; + let batch = ZcashSignBatch::new(request_id.clone(), payload.clone()); let cbor: Vec = batch.try_into().unwrap(); let encoded = probe_encode(&cbor, 100, ZcashSignBatch::get_registry_type().get_type()).unwrap(); @@ -270,23 +252,8 @@ mod tests { } let decoded_batch = decoded_batch.unwrap(); - assert_eq!(decoded_batch.get_version(), ZCASH_SIGN_BATCH_VERSION); - assert_eq!(decoded_batch.get_request_id(), &vec![0xaa, 0xbb]); - assert_eq!( - decoded_batch.get_network(), - ZCASH_SIGN_BATCH_NETWORK_MAINNET - ); - assert!(decoded_batch.get_atomic()); - assert_eq!(decoded_batch.get_messages().len(), 1); - assert_eq!( - decoded_batch.get_messages()[0].get_kind(), - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 - ); - assert_eq!(decoded_batch.get_messages()[0].get_payload(), &payload); - assert_eq!( - decoded_batch.get_messages()[0].get_payload_digest(), - Some(&payload_digest) - ); + assert_eq!(decoded_batch.get_data(), payload); + assert_eq!(decoded_batch.get_request_id(), request_id); } #[test] diff --git a/libs/ur-registry/src/registry_types.rs b/libs/ur-registry/src/registry_types.rs index 1f690c6..800eb55 100644 --- a/libs/ur-registry/src/registry_types.rs +++ b/libs/ur-registry/src/registry_types.rs @@ -33,6 +33,7 @@ pub enum URType { ZcashPczt(String), ZcashSignBatch(String), ZcashSignResult(String), + ZcashBatchSigResult(String), XmrOutput(String), XmrTxUnsigned(String), AvaxSignRequest(String), @@ -86,6 +87,7 @@ impl URType { "zcash-pczt" => Ok(URType::ZcashPczt(type_str.to_string())), "zcash-sign-batch" => Ok(URType::ZcashSignBatch(type_str.to_string())), "zcash-sign-result" => Ok(URType::ZcashSignResult(type_str.to_string())), + "zcash-batch-sig-result" => Ok(URType::ZcashBatchSigResult(type_str.to_string())), "tron-sign-request" => Ok(URType::TronSignRequest(type_str.to_string())), "tron-signature" => Ok(URType::TronSignature(type_str.to_string())), "xmr-output" => Ok(URType::XmrOutput(type_str.to_string())), @@ -132,6 +134,7 @@ impl URType { URType::ZcashPczt(type_str) => type_str.to_string(), URType::ZcashSignBatch(type_str) => type_str.to_string(), URType::ZcashSignResult(type_str) => type_str.to_string(), + URType::ZcashBatchSigResult(type_str) => type_str.to_string(), URType::TronSignRequest(type_str) => type_str.to_string(), URType::TronSignature(type_str) => type_str.to_string(), URType::XmrOutput(type_str) => type_str.to_string(), @@ -278,3 +281,5 @@ pub const ZCASH_UNIFIED_FULL_VIEWING_KEY: RegistryType = pub const ZCASH_PCZT: RegistryType = RegistryType("zcash-pczt", Some(49204)); pub const ZCASH_SIGN_BATCH: RegistryType = RegistryType("zcash-sign-batch", Some(49205)); pub const ZCASH_SIGN_RESULT: RegistryType = RegistryType("zcash-sign-result", Some(49206)); +pub const ZCASH_BATCH_SIG_RESULT: RegistryType = + RegistryType("zcash-batch-sig-result", Some(49207)); diff --git a/libs/ur-registry/src/zcash/cbor_helpers.rs b/libs/ur-registry/src/zcash/cbor_helpers.rs index e20ba75..6c5d141 100644 --- a/libs/ur-registry/src/zcash/cbor_helpers.rs +++ b/libs/ur-registry/src/zcash/cbor_helpers.rs @@ -1,6 +1,26 @@ use alloc::vec::Vec; use minicbor::Decoder; +/// Decodes a definite length CBOR map with `u8` keys. +pub(super) fn decode_definite_u8_map<'b, T, F>( + d: &mut Decoder<'b>, + obj: &mut T, + message: &'static str, + mut decode_entry: F, +) -> Result<(), minicbor::decode::Error> +where + F: FnMut(u8, &mut T, &mut Decoder<'b>) -> Result<(), minicbor::decode::Error>, +{ + let len = d + .map()? + .ok_or_else(|| minicbor::decode::Error::message(message).at(d.position()))?; + for _ in 0..len { + let key = d.u8()?; + decode_entry(key, obj, d)?; + } + Ok(()) +} + pub(super) fn reject_duplicate_key( seen_keys: &mut Vec, key: u8, diff --git a/libs/ur-registry/src/zcash/mod.rs b/libs/ur-registry/src/zcash/mod.rs index a1d4ca6..87c53ba 100644 --- a/libs/ur-registry/src/zcash/mod.rs +++ b/libs/ur-registry/src/zcash/mod.rs @@ -1,6 +1,7 @@ mod cbor_helpers; pub mod zcash_accounts; +pub mod zcash_batch_sig_result; pub mod zcash_pczt; pub mod zcash_sign_batch; pub mod zcash_sign_result; diff --git a/libs/ur-registry/src/zcash/zcash_batch_sig_result.rs b/libs/ur-registry/src/zcash/zcash_batch_sig_result.rs new file mode 100644 index 0000000..293fc90 --- /dev/null +++ b/libs/ur-registry/src/zcash/zcash_batch_sig_result.rs @@ -0,0 +1,254 @@ +//! Zcash PCZT batch signature result Registry Type. +//! +//! The payload is the opaque output of +//! `pczt::roles::signer::batch::BatchSignResponse::serialize`. The PCZT crate +//! owns the versioned response encoding and its semantic validation; this +//! registry type only provides the outer UR/CBOR envelope and the request id +//! copied from the corresponding signing request. + +use alloc::{string::ToString, vec::Vec}; + +use minicbor::data::Int; + +use crate::{ + error::{URError, URResult}, + registry_types::{RegistryType, ZCASH_BATCH_SIG_RESULT}, + traits::{MapSize, RegistryItem}, + types::Bytes, +}; + +use super::cbor_helpers::{decode_definite_u8_map, reject_duplicate_key, require_key}; + +const DATA: u8 = 1; +const REQUEST_ID: u8 = 2; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ZcashBatchSigResult { + data: Bytes, + request_id: Bytes, +} + +impl ZcashBatchSigResult { + pub fn new(request_id: Bytes, data: Bytes) -> Self { + Self { data, request_id } + } + + pub fn get_data(&self) -> &[u8] { + &self.data + } + + pub fn get_request_id(&self) -> &[u8] { + &self.request_id + } +} + +impl MapSize for ZcashBatchSigResult { + fn map_size(&self) -> u64 { + 2 + } +} + +impl RegistryItem for ZcashBatchSigResult { + fn get_registry_type() -> RegistryType<'static> { + ZCASH_BATCH_SIG_RESULT + } +} + +impl minicbor::Encode for ZcashBatchSigResult { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.map(self.map_size())?; + e.int(Int::from(DATA))?.bytes(&self.data)?; + e.int(Int::from(REQUEST_ID))?.bytes(&self.request_id)?; + Ok(()) + } +} + +impl<'b, C> minicbor::Decode<'b, C> for ZcashBatchSigResult { + fn decode( + d: &mut minicbor::Decoder<'b>, + _ctx: &mut C, + ) -> Result { + let mut result = ZcashBatchSigResult::default(); + let mut seen_keys = Vec::new(); + decode_definite_u8_map( + d, + &mut result, + "indefinite zcash-batch-sig-result map is unsupported", + |key, obj, d| { + reject_duplicate_key( + &mut seen_keys, + key, + d, + "duplicate key in zcash-batch-sig-result map", + )?; + match key { + DATA => obj.data = d.bytes()?.to_vec(), + REQUEST_ID => obj.request_id = d.bytes()?.to_vec(), + _ => d.skip()?, + } + Ok(()) + }, + )?; + require_key(&seen_keys, DATA, d, "missing zcash-batch-sig-result data")?; + require_key( + &seen_keys, + REQUEST_ID, + d, + "missing zcash-batch-sig-result request id", + )?; + Ok(result) + } +} + +impl TryFrom> for ZcashBatchSigResult { + type Error = URError; + + fn try_from(value: Vec) -> URResult { + let mut decoder = minicbor::Decoder::new(&value); + let result = + >::decode(&mut decoder, &mut ()) + .map_err(|e| URError::CborDecodeError(e.to_string()))?; + if decoder.position() != value.len() { + return Err(URError::CborDecodeError( + "trailing data after zcash-batch-sig-result".to_string(), + )); + } + Ok(result) + } +} + +impl TryInto> for ZcashBatchSigResult { + type Error = URError; + + fn try_into(self) -> URResult> { + minicbor::to_vec(self).map_err(|e| URError::CborEncodeError(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + // Serialization of an empty PCZT BatchSignResponse. The response format is + // "PCZS" || batch_version_le || Postcard body. + fn empty_batch_response() -> Vec { + vec![b'P', b'C', b'Z', b'S', 1, 0, 0, 0, 0] + } + + #[test] + fn round_trip() { + let data = empty_batch_response(); + let request_id = vec![0xaa, 0xbb]; + let result = ZcashBatchSigResult::new(request_id.clone(), data.clone()); + + let encoded: Vec = result.try_into().unwrap(); + let decoded = ZcashBatchSigResult::try_from(encoded).unwrap(); + + assert_eq!(decoded.get_data(), data); + assert_eq!(decoded.get_request_id(), request_id); + } + + #[test] + fn wire_encoding_is_stable() { + let encoded: Vec = ZcashBatchSigResult::new(vec![0xaa, 0xbb], empty_batch_response()) + .try_into() + .unwrap(); + + assert_eq!(hex::encode(encoded), "a2014950435a5301000000000242aabb"); + } + + #[test] + fn preserves_empty_data() { + let encoded: Vec = ZcashBatchSigResult::new(vec![0xaa, 0xbb], vec![]) + .try_into() + .unwrap(); + let decoded = ZcashBatchSigResult::try_from(encoded).unwrap(); + + assert!(decoded.get_data().is_empty()); + } + + #[test] + fn preserves_empty_request_id() { + let encoded: Vec = ZcashBatchSigResult::new(vec![], empty_batch_response()) + .try_into() + .unwrap(); + let decoded = ZcashBatchSigResult::try_from(encoded).unwrap(); + + assert!(decoded.get_request_id().is_empty()); + } + + #[test] + fn rejects_missing_data() { + let err = ZcashBatchSigResult::try_from(vec![0xa1, REQUEST_ID, 0x40]).unwrap_err(); + + assert!( + err.to_string() + .contains("missing zcash-batch-sig-result data") + ); + } + + #[test] + fn rejects_missing_request_id() { + let err = ZcashBatchSigResult::try_from(vec![0xa1, DATA, 0x40]).unwrap_err(); + + assert!( + err.to_string() + .contains("missing zcash-batch-sig-result request id") + ); + } + + #[test] + fn rejects_duplicate_keys() { + let err = ZcashBatchSigResult::try_from(vec![0xa2, 0x01, 0x40, 0x01, 0x40]).unwrap_err(); + + assert!( + err.to_string() + .contains("duplicate key in zcash-batch-sig-result map") + ); + } + + #[test] + fn rejects_indefinite_map() { + let err = ZcashBatchSigResult::try_from(vec![0xbf, DATA, 0x40, REQUEST_ID, 0x40, 0xff]) + .unwrap_err(); + + assert!( + err.to_string() + .contains("indefinite zcash-batch-sig-result map is unsupported") + ); + } + + #[test] + fn rejects_trailing_data() { + let result = ZcashBatchSigResult::new(vec![0xaa, 0xbb], empty_batch_response()); + let mut encoded: Vec = result.try_into().unwrap(); + encoded.push(0); + + let err = ZcashBatchSigResult::try_from(encoded).unwrap_err(); + + assert!(err.to_string().contains("trailing data")); + } + + #[test] + fn skips_unknown_keys() { + let encoded = hex::decode("a3014950435a5301000000000242aabb0982010a").unwrap(); + + let decoded = ZcashBatchSigResult::try_from(encoded).unwrap(); + + assert_eq!(decoded.get_data(), empty_batch_response()); + assert_eq!(decoded.get_request_id(), &[0xaa, 0xbb]); + } + + #[test] + fn registry_type() { + assert_eq!( + ZcashBatchSigResult::get_registry_type().get_type(), + "zcash-batch-sig-result" + ); + } +} diff --git a/libs/ur-registry/src/zcash/zcash_sign_batch.rs b/libs/ur-registry/src/zcash/zcash_sign_batch.rs index 2d0006c..d4c56e5 100644 --- a/libs/ur-registry/src/zcash/zcash_sign_batch.rs +++ b/libs/ur-registry/src/zcash/zcash_sign_batch.rs @@ -1,103 +1,50 @@ -//! Zcash signing batch Registry Type. +//! Zcash PCZT batch signing request Registry Type. //! -//! This module implements CBOR encoding and decoding for batches of Zcash -//! signing messages. Each message carries a stable message id, a kind, and the -//! raw payload that should be signed. -//! -//! This is a registry container, not a protocol policy validator. Decode checks -//! CBOR shape, required fields, duplicate CBOR map keys, and trailing data, then -//! preserves registry values as supplied. Callers enforce policy such as -//! supported versions, networks, message kinds, unique message ids, digest -//! validity, and batch signing semantics. +//! The payload is the opaque output of +//! `pczt::roles::signer::batch::BatchSignRequest::serialize`. The PCZT crate +//! owns the versioned request encoding and its semantic validation; this +//! registry type only provides the outer UR/CBOR envelope and the request id +//! used to correlate it with a signing result. + +use alloc::{string::ToString, vec::Vec}; + +use minicbor::data::Int; -use super::cbor_helpers::{reject_duplicate_key, require_key}; use crate::{ + error::{URError, URResult}, registry_types::{RegistryType, ZCASH_SIGN_BATCH}, traits::{MapSize, RegistryItem}, + types::Bytes, }; -use alloc::string::ToString; -use alloc::vec; -use alloc::vec::Vec; -use minicbor::data::Int; -use minicbor::{Decoder, Encoder}; - -use crate::error::{URError, URResult}; -/// Registered batch version used by producers. Decode preserves any `u32` -/// version so callers can decide protocol policy. -pub const ZCASH_SIGN_BATCH_VERSION: u32 = 1; -/// Registered mainnet network value used by producers. Decode preserves any -/// `u32` network so callers can decide protocol policy. -pub const ZCASH_SIGN_BATCH_NETWORK_MAINNET: u32 = 1; -/// Registered PCZT v1 message kind used by producers. Decode preserves any -/// `u32` kind so callers can decide protocol policy. -pub const ZCASH_SIGN_MESSAGE_KIND_PCZT_V1: u32 = 1; +use super::cbor_helpers::{decode_definite_u8_map, reject_duplicate_key, require_key}; -const VERSION: u8 = 1; +const DATA: u8 = 1; const REQUEST_ID: u8 = 2; -const NETWORK: u8 = 3; -const MESSAGES: u8 = 4; -const ATOMIC: u8 = 11; -const MESSAGE_ID: u8 = 1; -const MESSAGE_KIND: u8 = 2; -const MESSAGE_PAYLOAD: u8 = 3; -const MESSAGE_PAYLOAD_DIGEST: u8 = 6; - -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct ZcashSignBatch { - version: u32, - request_id: Vec, - network: u32, - messages: Vec, - atomic: Option, + data: Bytes, + request_id: Bytes, } impl ZcashSignBatch { - /// Builds a signing batch container. The SDK does not validate protocol - /// policy such as supported version, network, or duplicate message ids here. - pub fn new( - version: u32, - request_id: Vec, - network: u32, - messages: Vec, - atomic: Option, - ) -> Self { - Self { - version, - request_id, - network, - messages, - atomic, - } + pub fn new(request_id: Bytes, data: Bytes) -> Self { + Self { data, request_id } } - pub fn get_version(&self) -> u32 { - self.version + pub fn get_data(&self) -> &[u8] { + &self.data } - pub fn get_request_id(&self) -> &Vec { + pub fn get_request_id(&self) -> &[u8] { &self.request_id } +} - pub fn get_network(&self) -> u32 { - self.network - } - - pub fn get_messages(&self) -> &Vec { - &self.messages - } - - /// Returns the optional `atomic` field exactly as it appeared in the - /// registry container. Use `get_atomic` for the effective default. - pub fn get_atomic_field(&self) -> Option { - self.atomic - } - - /// Returns the effective batch semantics. An omitted `atomic` field - /// defaults to `true`. - pub fn get_atomic(&self) -> bool { - self.atomic.unwrap_or(true) +impl MapSize for ZcashSignBatch { + fn map_size(&self) -> u64 { + 2 } } @@ -107,60 +54,53 @@ impl RegistryItem for ZcashSignBatch { } } -impl MapSize for ZcashSignBatch { - fn map_size(&self) -> u64 { - if self.atomic.is_some() { - 5 - } else { - 4 - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct ZcashSignMessage { - id: Vec, - kind: u32, - payload: Vec, - payload_digest: Option>, -} - -impl ZcashSignMessage { - /// Builds a signing message container. The SDK does not validate protocol - /// policy such as supported kind, id uniqueness, or digest length here. - pub fn new(id: Vec, kind: u32, payload: Vec, payload_digest: Option>) -> Self { - Self { - id, - kind, - payload, - payload_digest, - } - } - - pub fn get_id(&self) -> &Vec { - &self.id - } - - pub fn get_kind(&self) -> u32 { - self.kind - } - - pub fn get_payload(&self) -> &Vec { - &self.payload - } - - pub fn get_payload_digest(&self) -> Option<&Vec> { - self.payload_digest.as_ref() +impl minicbor::Encode for ZcashSignBatch { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.map(self.map_size())?; + e.int(Int::from(DATA))?.bytes(&self.data)?; + e.int(Int::from(REQUEST_ID))?.bytes(&self.request_id)?; + Ok(()) } } -impl MapSize for ZcashSignMessage { - fn map_size(&self) -> u64 { - if self.payload_digest.is_some() { - 4 - } else { - 3 - } +impl<'b, C> minicbor::Decode<'b, C> for ZcashSignBatch { + fn decode( + d: &mut minicbor::Decoder<'b>, + _ctx: &mut C, + ) -> Result { + let mut result = ZcashSignBatch::default(); + let mut seen_keys = Vec::new(); + decode_definite_u8_map( + d, + &mut result, + "indefinite zcash-sign-batch map is unsupported", + |key, obj, d| { + reject_duplicate_key( + &mut seen_keys, + key, + d, + "duplicate key in zcash-sign-batch map", + )?; + match key { + DATA => obj.data = d.bytes()?.to_vec(), + REQUEST_ID => obj.request_id = d.bytes()?.to_vec(), + _ => d.skip()?, + } + Ok(()) + }, + )?; + require_key(&seen_keys, DATA, d, "missing zcash-sign-batch data")?; + require_key( + &seen_keys, + REQUEST_ID, + d, + "missing zcash-sign-batch request id", + )?; + Ok(result) } } @@ -168,7 +108,7 @@ impl TryFrom> for ZcashSignBatch { type Error = URError; fn try_from(value: Vec) -> URResult { - let mut decoder = Decoder::new(&value); + let mut decoder = minicbor::Decoder::new(&value); let batch = >::decode(&mut decoder, &mut ()) .map_err(|e| URError::CborDecodeError(e.to_string()))?; if decoder.position() != value.len() { @@ -188,473 +128,124 @@ impl TryInto> for ZcashSignBatch { } } -impl minicbor::Encode for ZcashSignBatch { - fn encode( - &self, - e: &mut Encoder, - ctx: &mut C, - ) -> Result<(), minicbor::encode::Error> { - e.map(self.map_size())?; - e.int(Int::from(VERSION))?.u32(self.version)?; - e.int(Int::from(REQUEST_ID))?.bytes(&self.request_id)?; - e.int(Int::from(NETWORK))?.u32(self.network)?; - e.int(Int::from(MESSAGES))? - .array(self.messages.len() as u64)?; - for message in &self.messages { - message.encode(e, ctx)?; - } - if let Some(atomic) = self.atomic { - e.int(Int::from(ATOMIC))?.bool(atomic)?; - } - Ok(()) - } -} - -impl minicbor::Encode for ZcashSignMessage { - fn encode( - &self, - e: &mut Encoder, - _ctx: &mut C, - ) -> Result<(), minicbor::encode::Error> { - e.map(self.map_size())?; - e.int(Int::from(MESSAGE_ID))?.bytes(&self.id)?; - e.int(Int::from(MESSAGE_KIND))?.u32(self.kind)?; - e.int(Int::from(MESSAGE_PAYLOAD))?.bytes(&self.payload)?; - if let Some(payload_digest) = self.payload_digest.as_ref() { - e.int(Int::from(MESSAGE_PAYLOAD_DIGEST))? - .bytes(payload_digest)?; - } - Ok(()) - } -} - -impl<'b, C> minicbor::Decode<'b, C> for ZcashSignBatch { - fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result { - let mut result = ZcashSignBatch::default(); - let len = d.map()?.ok_or_else(|| { - minicbor::decode::Error::message("indefinite zcash-sign-batch map is unsupported") - .at(d.position()) - })?; - let mut seen_keys = Vec::new(); - for _ in 0..len { - let key = d.u8()?; - reject_duplicate_key( - &mut seen_keys, - key, - d, - "duplicate key in zcash-sign-batch map", - )?; - match key { - VERSION => result.version = d.u32()?, - REQUEST_ID => result.request_id = d.bytes()?.to_vec(), - NETWORK => result.network = d.u32()?, - MESSAGES => { - let mut messages = vec![]; - let len = d.array()?.ok_or_else(|| { - minicbor::decode::Error::message( - "indefinite zcash-sign-batch messages array is unsupported", - ) - .at(d.position()) - })?; - for _ in 0..len { - messages.push(ZcashSignMessage::decode(d, ctx)?); - } - result.messages = messages; - } - ATOMIC => result.atomic = Some(d.bool()?), - _ => d.skip()?, - } - } - require_key(&seen_keys, VERSION, d, "missing zcash-sign-batch version")?; - require_key( - &seen_keys, - REQUEST_ID, - d, - "missing zcash-sign-batch request id", - )?; - require_key(&seen_keys, NETWORK, d, "missing zcash-sign-batch network")?; - require_key(&seen_keys, MESSAGES, d, "missing zcash-sign-batch messages")?; - Ok(result) - } -} - -impl<'b, C> minicbor::Decode<'b, C> for ZcashSignMessage { - fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result { - let mut result = ZcashSignMessage::default(); - let len = d.map()?.ok_or_else(|| { - minicbor::decode::Error::message("indefinite zcash-sign-message map is unsupported") - .at(d.position()) - })?; - let mut seen_keys = Vec::new(); - for _ in 0..len { - let key = d.u8()?; - reject_duplicate_key( - &mut seen_keys, - key, - d, - "duplicate key in zcash-sign-message map", - )?; - match key { - MESSAGE_ID => result.id = d.bytes()?.to_vec(), - MESSAGE_KIND => result.kind = d.u32()?, - MESSAGE_PAYLOAD => result.payload = d.bytes()?.to_vec(), - MESSAGE_PAYLOAD_DIGEST => result.payload_digest = Some(d.bytes()?.to_vec()), - _ => d.skip()?, - } - } - require_key(&seen_keys, MESSAGE_ID, d, "missing zcash-sign-message id")?; - require_key( - &seen_keys, - MESSAGE_KIND, - d, - "missing zcash-sign-message kind", - )?; - require_key( - &seen_keys, - MESSAGE_PAYLOAD, - d, - "missing zcash-sign-message payload", - )?; - Ok(result) - } -} - #[cfg(test)] mod tests { use super::*; use alloc::vec; - #[test] - fn test_zcash_sign_batch_encode_decode() { - let payload_digest = - hex::decode("ee9040f65c341855e070ff438eb0ea9d5b831b2a2c270fb7ef592d750408e3b3") - .unwrap(); - let batch = ZcashSignBatch::new( - ZCASH_SIGN_BATCH_VERSION, - vec![0xaa, 0xbb], - ZCASH_SIGN_BATCH_NETWORK_MAINNET, - vec![ZcashSignMessage::new( - vec![0x01], - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - vec![0x02, 0x03], - Some(payload_digest.clone()), - )], - Some(false), - ); - - let encoded: Vec = batch.clone().try_into().unwrap(); - let decoded = ZcashSignBatch::try_from(encoded).unwrap(); - - assert_eq!(decoded.get_version(), ZCASH_SIGN_BATCH_VERSION); - assert_eq!(decoded.get_request_id(), &vec![0xaa, 0xbb]); - assert_eq!(decoded.get_network(), ZCASH_SIGN_BATCH_NETWORK_MAINNET); - assert_eq!(decoded.get_atomic_field(), Some(false)); - assert!(!decoded.get_atomic()); - assert_eq!(decoded.get_messages().len(), 1); - assert_eq!(decoded.get_messages()[0].get_id(), &vec![0x01]); - assert_eq!( - decoded.get_messages()[0].get_kind(), - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 - ); - assert_eq!(decoded.get_messages()[0].get_payload(), &vec![0x02, 0x03]); - assert_eq!( - decoded.get_messages()[0].get_payload_digest(), - Some(&payload_digest) - ); + // Serialization of an empty PCZT BatchSignRequest. The request format is + // "PCZB" || batch_version_le || pczt_version_le || Postcard body. + fn empty_batch_request() -> Vec { + vec![b'P', b'C', b'Z', b'B', 1, 0, 0, 0, 2, 0, 0, 0, 0] } #[test] - fn test_zcash_sign_batch_decodes_literal_cbor_fixture() { - let cbor = hex::decode( - "a501010242aabb03010481a40141010201034c70637a742d726571756573740658207a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da40bf4", - ) - .unwrap(); - let payload_digest = - hex::decode("7a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da4") - .unwrap(); - - let decoded = ZcashSignBatch::try_from(cbor.clone()).unwrap(); - - assert_eq!(decoded.get_version(), ZCASH_SIGN_BATCH_VERSION); - assert_eq!(decoded.get_request_id(), &vec![0xaa, 0xbb]); - assert_eq!(decoded.get_network(), ZCASH_SIGN_BATCH_NETWORK_MAINNET); - assert_eq!(decoded.get_atomic_field(), Some(false)); - assert!(!decoded.get_atomic()); - assert_eq!(decoded.get_messages().len(), 1); - assert_eq!(decoded.get_messages()[0].get_id(), &vec![0x01]); - assert_eq!( - decoded.get_messages()[0].get_kind(), - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 - ); - assert_eq!( - decoded.get_messages()[0].get_payload(), - &b"pczt-request".to_vec() - ); - assert_eq!( - decoded.get_messages()[0].get_payload_digest(), - Some(&payload_digest) - ); - assert_eq!( - decoded.get_messages()[0] - .get_payload_digest() - .unwrap() - .len(), - 32 - ); + fn round_trip() { + let data = empty_batch_request(); + let request_id = vec![0xaa, 0xbb]; + let batch = ZcashSignBatch::new(request_id.clone(), data.clone()); - let batch = ZcashSignBatch::new( - ZCASH_SIGN_BATCH_VERSION, - vec![0xaa, 0xbb], - ZCASH_SIGN_BATCH_NETWORK_MAINNET, - vec![ZcashSignMessage::new( - vec![0x01], - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - b"pczt-request".to_vec(), - Some(payload_digest), - )], - Some(false), - ); let encoded: Vec = batch.try_into().unwrap(); + let decoded = ZcashSignBatch::try_from(encoded).unwrap(); - assert_eq!(encoded, cbor); - } - - #[test] - fn test_zcash_sign_batch_skips_unknown_fields() { - let fixtures = [ - // Unknown top level key 9 with value [1, {"x": true}]. - "a601010242aabb03010481a40141010201034c70637a742d726571756573740658207a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da4098201a16178f50bf4", - // Unknown nested message key 9 with value [1, {"x": true}]. - "a501010242aabb03010481a50141010201034c70637a742d72657175657374098201a16178f50658207a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da40bf4", - ]; - let payload_digest = - hex::decode("7a66e6be087afee0665161828bd11dc1e201fdb8c2d72786ee485e29897c8da4") - .unwrap(); - - for cbor_hex in fixtures { - let decoded = ZcashSignBatch::try_from(hex::decode(cbor_hex).unwrap()).unwrap(); - - assert_eq!(decoded.get_version(), ZCASH_SIGN_BATCH_VERSION); - assert_eq!(decoded.get_request_id(), &vec![0xaa, 0xbb]); - assert_eq!(decoded.get_network(), ZCASH_SIGN_BATCH_NETWORK_MAINNET); - assert_eq!(decoded.get_atomic_field(), Some(false)); - assert_eq!(decoded.get_messages().len(), 1); - assert_eq!(decoded.get_messages()[0].get_id(), &vec![0x01]); - assert_eq!( - decoded.get_messages()[0].get_kind(), - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 - ); - assert_eq!( - decoded.get_messages()[0].get_payload(), - &b"pczt-request".to_vec() - ); - assert_eq!( - decoded.get_messages()[0].get_payload_digest(), - Some(&payload_digest) - ); - } + assert_eq!(decoded.get_data(), data); + assert_eq!(decoded.get_request_id(), request_id); } #[test] - fn test_zcash_sign_batch_decodes_unknown_policy_values() { - let batch = ZcashSignBatch::new( - 99, - vec![0xaa, 0xbb], - 42, - vec![ZcashSignMessage::new( - vec![0x01], - 77, - b"policy-is-external".to_vec(), - None, - )], - Some(true), - ); - let encoded: Vec = batch.try_into().unwrap(); - - let decoded = ZcashSignBatch::try_from(encoded).unwrap(); + fn wire_encoding_is_stable() { + let encoded: Vec = ZcashSignBatch::new(vec![0xaa, 0xbb], empty_batch_request()) + .try_into() + .unwrap(); - assert_eq!(decoded.get_version(), 99); - assert_eq!(decoded.get_request_id(), &vec![0xaa, 0xbb]); - assert_eq!(decoded.get_network(), 42); - assert_eq!(decoded.get_atomic_field(), Some(true)); - assert!(decoded.get_atomic()); - assert_eq!(decoded.get_messages().len(), 1); - assert_eq!(decoded.get_messages()[0].get_id(), &vec![0x01]); - assert_eq!(decoded.get_messages()[0].get_kind(), 77); assert_eq!( - decoded.get_messages()[0].get_payload(), - &b"policy-is-external".to_vec() + hex::encode(encoded), + "a2014d50435a420100000002000000000242aabb" ); } #[test] - fn test_zcash_sign_batch_decodes_duplicate_message_ids() { - let batch = ZcashSignBatch::new( - ZCASH_SIGN_BATCH_VERSION, - vec![0xaa, 0xbb], - ZCASH_SIGN_BATCH_NETWORK_MAINNET, - vec![ - ZcashSignMessage::new( - vec![0x01], - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - b"first".to_vec(), - None, - ), - ZcashSignMessage::new( - vec![0x01], - ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, - b"second".to_vec(), - None, - ), - ], - Some(true), - ); - let encoded: Vec = batch.try_into().unwrap(); - + fn preserves_empty_data() { + let encoded: Vec = ZcashSignBatch::new(vec![0xaa, 0xbb], vec![]) + .try_into() + .unwrap(); let decoded = ZcashSignBatch::try_from(encoded).unwrap(); - assert_eq!(decoded.get_messages().len(), 2); - assert_eq!(decoded.get_messages()[0].get_id(), &vec![0x01]); - assert_eq!(decoded.get_messages()[1].get_id(), &vec![0x01]); - assert_eq!(decoded.get_messages()[0].get_payload(), &b"first".to_vec()); - assert_eq!(decoded.get_messages()[1].get_payload(), &b"second".to_vec()); + assert!(decoded.get_data().is_empty()); } #[test] - fn test_zcash_sign_batch_defaults_to_atomic() { - let batch = ZcashSignBatch::new(1, vec![], 1, vec![], None); - let encoded: Vec = batch.try_into().unwrap(); + fn preserves_empty_request_id() { + let encoded: Vec = ZcashSignBatch::new(vec![], empty_batch_request()) + .try_into() + .unwrap(); let decoded = ZcashSignBatch::try_from(encoded).unwrap(); - assert_eq!(decoded.get_atomic_field(), None); - assert!(decoded.get_atomic()); + assert!(decoded.get_request_id().is_empty()); } #[test] - fn test_zcash_sign_batch_rejects_duplicate_keys() { - let duplicate_version_keys = vec![0xa2, VERSION, 0x01, VERSION, 0x02]; - - let err = ZcashSignBatch::try_from(duplicate_version_keys).unwrap_err(); + fn rejects_missing_data() { + let err = ZcashSignBatch::try_from(vec![0xa1, REQUEST_ID, 0x40]).unwrap_err(); - assert!(err.to_string().contains("duplicate key")); + assert!(err.to_string().contains("missing zcash-sign-batch data")); } #[test] - fn test_zcash_sign_batch_rejects_duplicate_message_keys() { - let duplicate_message_id_keys = vec![ - 0xa4, - VERSION, - 0x01, - REQUEST_ID, - 0x40, - NETWORK, - 0x01, - MESSAGES, - 0x81, - 0xa4, - MESSAGE_ID, - 0x40, - MESSAGE_ID, - 0x40, - MESSAGE_KIND, - 0x01, - MESSAGE_PAYLOAD, - 0x40, - ]; - - let err = ZcashSignBatch::try_from(duplicate_message_id_keys).unwrap_err(); - - assert!(err - .to_string() - .contains("duplicate key in zcash-sign-message map")); - } - - #[test] - fn test_zcash_sign_batch_rejects_trailing_data() { - let batch = ZcashSignBatch::new(1, vec![], 1, vec![], None); - let mut encoded: Vec = batch.try_into().unwrap(); - encoded.push(0x00); - - let err = ZcashSignBatch::try_from(encoded).unwrap_err(); + fn rejects_missing_request_id() { + let err = ZcashSignBatch::try_from(vec![0xa1, DATA, 0x40]).unwrap_err(); - assert!(err.to_string().contains("trailing data")); + assert!( + err.to_string() + .contains("missing zcash-sign-batch request id") + ); } #[test] - fn test_zcash_sign_batch_rejects_indefinite_top_level_map() { - let indefinite_map = vec![0xbf, 0xff]; + fn rejects_duplicate_keys() { + let err = ZcashSignBatch::try_from(vec![0xa2, 0x01, 0x40, 0x01, 0x40]).unwrap_err(); - let err = ZcashSignBatch::try_from(indefinite_map).unwrap_err(); - - assert!(err.to_string().contains("indefinite zcash-sign-batch map")); + assert!( + err.to_string() + .contains("duplicate key in zcash-sign-batch map") + ); } #[test] - fn test_zcash_sign_batch_rejects_indefinite_messages_array() { - let indefinite_messages = vec![ - 0xa4, VERSION, 0x01, REQUEST_ID, 0x40, NETWORK, 0x01, MESSAGES, 0x9f, 0xff, - ]; + fn rejects_indefinite_map() { + let err = + ZcashSignBatch::try_from(vec![0xbf, DATA, 0x40, REQUEST_ID, 0x40, 0xff]).unwrap_err(); - let err = ZcashSignBatch::try_from(indefinite_messages).unwrap_err(); - - assert!(err.to_string().contains("messages array")); + assert!( + err.to_string() + .contains("indefinite zcash-sign-batch map is unsupported") + ); } #[test] - fn test_zcash_sign_batch_rejects_indefinite_message_map() { - let indefinite_message = vec![ - 0xa4, VERSION, 0x01, REQUEST_ID, 0x40, NETWORK, 0x01, MESSAGES, 0x81, 0xbf, 0xff, - ]; + fn rejects_trailing_data() { + let mut encoded: Vec = ZcashSignBatch::new(vec![0xaa, 0xbb], empty_batch_request()) + .try_into() + .unwrap(); + encoded.push(0); - let err = ZcashSignBatch::try_from(indefinite_message).unwrap_err(); + let err = ZcashSignBatch::try_from(encoded).unwrap_err(); - assert!(err - .to_string() - .contains("indefinite zcash-sign-message map")); + assert!(err.to_string().contains("trailing data")); } #[test] - fn test_zcash_sign_batch_rejects_missing_required_top_level_key() { - for (cbor_hex, message) in [ - ("a3024003010480", "missing zcash-sign-batch version"), - ("a3010103010480", "missing zcash-sign-batch request id"), - ("a3010102400480", "missing zcash-sign-batch network"), - ("a3010102400301", "missing zcash-sign-batch messages"), - ] { - let cbor = hex::decode(cbor_hex).unwrap(); - - let err = ZcashSignBatch::try_from(cbor).unwrap_err(); - - assert!(err.to_string().contains(message)); - } - } + fn skips_unknown_keys() { + let encoded = hex::decode("a3014d50435a420100000002000000000242aabb0982010a").unwrap(); - #[test] - fn test_zcash_sign_batch_rejects_missing_required_message_key() { - for (cbor_hex, message) in [ - ( - "a40101024003010481a202010340", - "missing zcash-sign-message id", - ), - ( - "a40101024003010481a201400340", - "missing zcash-sign-message kind", - ), - ( - "a40101024003010481a201400201", - "missing zcash-sign-message payload", - ), - ] { - let cbor = hex::decode(cbor_hex).unwrap(); - - let err = ZcashSignBatch::try_from(cbor).unwrap_err(); - - assert!(err.to_string().contains(message)); - } + let decoded = ZcashSignBatch::try_from(encoded).unwrap(); + + assert_eq!(decoded.get_data(), empty_batch_request()); + assert_eq!(decoded.get_request_id(), &[0xaa, 0xbb]); } #[test] - fn test_registry_type() { + fn registry_type() { assert_eq!( ZcashSignBatch::get_registry_type().get_type(), "zcash-sign-batch" diff --git a/libs/ur-registry/src/zcash/zcash_sign_result.rs b/libs/ur-registry/src/zcash/zcash_sign_result.rs index 3ab59d2..b207667 100644 --- a/libs/ur-registry/src/zcash/zcash_sign_result.rs +++ b/libs/ur-registry/src/zcash/zcash_sign_result.rs @@ -9,10 +9,7 @@ //! correlation, supported versions, result status, result kind, unique ids, //! digest validity, and expected result count. -use super::{ - cbor_helpers::{reject_duplicate_key, require_key}, - zcash_sign_batch::ZCASH_SIGN_MESSAGE_KIND_PCZT_V1, -}; +use super::cbor_helpers::{reject_duplicate_key, require_key}; use crate::{ registry_types::{RegistryType, ZCASH_SIGN_RESULT}, traits::{MapSize, RegistryItem}, @@ -30,7 +27,7 @@ use crate::error::{URError, URResult}; pub const ZCASH_SIGN_RESULT_VERSION: u32 = 1; /// Registered PCZT v1 result kind used by producers. Result kind mirrors the /// request message kind so callers can correlate request and response policy. -pub const ZCASH_SIGN_RESULT_KIND_PCZT_V1: u32 = ZCASH_SIGN_MESSAGE_KIND_PCZT_V1; +pub const ZCASH_SIGN_RESULT_KIND_PCZT_V1: u32 = 1; /// Registered status for a signed result. Decode preserves any `u32` status so /// callers can decide protocol policy. pub const ZCASH_SIGN_STATUS_SIGNED: u32 = 0;