Skip to main content

pczt/roles/signer/
batch.rs

1//! Postcard-encoded messages for batched external PCZT signing.
2//!
3//! Responses only represent Orchard-protocol spend authorization signatures for the
4//! Orchard and Ironwood value pools. Sapling spend authorization signatures are not
5//! represented.
6//!
7//! Request and response correlation is the responsibility of the application transport.
8//! Requests encode one shared PCZT version followed by headerless, version-specific PCZT
9//! payloads, so every PCZT in a batch uses the same encoding version.
10//!
11//! The request encoding is `"PCZB" || batch_version_le || pczt_version_le || body`, and
12//! the response encoding is `"PCZS" || batch_version_le || body`. Both version fields
13//! are four-byte little-endian integers, and each body uses the Postcard wire format.
14
15use alloc::vec::Vec;
16
17use serde::{Deserialize, Serialize};
18
19use super::SpendAuthSignature;
20use crate::Pczt;
21
22/// The current batched PCZT signing wire version.
23pub const VERSION: u32 = 1;
24
25const REQUEST_MAGIC_BYTES: &[u8; 4] = b"PCZB"; // PCZT batch.
26const RESPONSE_MAGIC_BYTES: &[u8; 4] = b"PCZS"; // PCZT signatures.
27
28/// A request to sign several PCZTs as one operation.
29///
30/// The PCZTs retain their caller-provided order. Protocol policy such as unique
31/// PCZTs, batch size, and all-or-nothing behavior is enforced by the application
32/// transporting the request.
33#[derive(Clone, Debug)]
34pub struct BatchSignRequest {
35    pczts: Vec<Pczt>,
36}
37
38impl BatchSignRequest {
39    /// Constructs a batched PCZT signing request.
40    pub fn new(pczts: Vec<Pczt>) -> Self {
41        Self { pczts }
42    }
43
44    /// Returns the PCZTs to sign, in request order.
45    pub fn pczts(&self) -> &[Pczt] {
46        &self.pczts
47    }
48
49    /// Parses a Postcard-encoded batched PCZT signing request.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the batch encoding is invalid or contains a PCZT that
54    /// cannot be parsed.
55    pub fn parse(bytes: &[u8]) -> Result<Self, ParseError> {
56        let body = parse_version(bytes, REQUEST_MAGIC_BYTES)?;
57        let (pczt_version, body) = parse_pczt_version(body)?;
58        match pczt_version {
59            crate::PCZT_VERSION_1 => {
60                Ok(parse_body::<v1::BatchSignRequest<crate::v1::Pczt>>(body)?.into())
61            }
62            crate::PCZT_VERSION_2 => {
63                Self::try_from(parse_body::<v1::BatchSignRequest<crate::v2::Pczt>>(body)?)
64            }
65            _ => Err(ParseError::UnknownPcztVersion(pczt_version)),
66        }
67    }
68
69    /// Serializes this request using the current Postcard wire version.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if a PCZT cannot be serialized using the current PCZT
74    /// encoding.
75    pub fn serialize(&self) -> Result<Vec<u8>, EncodingError> {
76        Ok(serialize_request(
77            crate::PCZT_VERSION_2,
78            &v1::BatchSignRequest::<crate::v2::Pczt>::try_from(self)?,
79        ))
80    }
81}
82
83/// The signatures produced for a [`BatchSignRequest`], in request order.
84/// Entry `i` contains the signatures produced for PCZT `i` in the request.
85///
86/// Only Orchard and Ironwood spend authorization signatures are represented; Sapling
87/// spend authorization signatures are not supported by this response format.
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct BatchSignResponse {
90    signatures: Vec<Vec<SpendAuthSignature>>,
91}
92
93impl BatchSignResponse {
94    /// Constructs a batched PCZT signing response.
95    pub fn new(signatures: Vec<Vec<SpendAuthSignature>>) -> Self {
96        Self { signatures }
97    }
98
99    /// Returns the Orchard and Ironwood signatures produced for each request PCZT, in
100    /// request order.
101    pub fn signatures(&self) -> &[Vec<SpendAuthSignature>] {
102        &self.signatures
103    }
104
105    /// Parses a Postcard-encoded batched PCZT signing response.
106    pub fn parse(bytes: &[u8]) -> Result<Self, ParseError> {
107        let body = parse_version(bytes, RESPONSE_MAGIC_BYTES)?;
108        let response = parse_body::<v1::BatchSignResponse>(body)?;
109        Self::try_from(response)
110    }
111
112    /// Serializes this response using the current Postcard wire version.
113    pub fn serialize(&self) -> Result<Vec<u8>, EncodingError> {
114        Ok(serialize_versioned(
115            RESPONSE_MAGIC_BYTES,
116            &v1::BatchSignResponse::try_from(self)?,
117        ))
118    }
119}
120
121/// Errors that can occur while encoding batched PCZT signing messages.
122#[derive(Debug)]
123#[non_exhaustive]
124pub enum EncodingError {
125    /// A signature's action index cannot be represented by the v1 wire format.
126    ActionIndexOutOfRange,
127    /// A PCZT cannot be represented by the current PCZT encoding.
128    PcztEncoding(crate::EncodingError),
129}
130
131/// Errors that can occur while parsing batched PCZT signing messages.
132#[derive(Debug)]
133#[non_exhaustive]
134pub enum ParseError {
135    /// The Postcard body is invalid.
136    Invalid(postcard::Error),
137    /// The bytes do not contain the expected batched PCZT signing message.
138    InvalidMagic,
139    /// A signature's action index cannot be represented on this platform.
140    ActionIndexOutOfRange,
141    /// An encoded PCZT is invalid.
142    PcztParse(crate::ParseError),
143    /// Bytes remain after the complete message body.
144    TrailingData,
145    /// The bytes are too short to contain a batched PCZT signing message header.
146    TooShort,
147    /// The request uses an unsupported PCZT wire version.
148    UnknownPcztVersion(u32),
149    /// The message uses an unsupported wire version.
150    UnknownVersion(u32),
151}
152
153fn parse_version<'a>(bytes: &'a [u8], magic: &[u8; 4]) -> Result<&'a [u8], ParseError> {
154    let (version, body) = crate::parse_header(bytes, magic).map_err(|e| match e {
155        crate::HeaderParseError::InvalidMagic => ParseError::InvalidMagic,
156        crate::HeaderParseError::TooShort => ParseError::TooShort,
157    })?;
158    if version != VERSION {
159        return Err(ParseError::UnknownVersion(version));
160    }
161    Ok(body)
162}
163
164fn parse_pczt_version(bytes: &[u8]) -> Result<(u32, &[u8]), ParseError> {
165    let version = bytes
166        .get(..4)
167        .ok_or(ParseError::TooShort)
168        .map(|version| u32::from_le_bytes(version.try_into().unwrap()))?;
169    Ok((version, &bytes[4..]))
170}
171
172fn parse_body<'a, T: Deserialize<'a>>(bytes: &'a [u8]) -> Result<T, ParseError> {
173    let (value, remaining) = postcard::take_from_bytes(bytes).map_err(ParseError::Invalid)?;
174    if !remaining.is_empty() {
175        return Err(ParseError::TrailingData);
176    }
177    Ok(value)
178}
179
180fn serialize_request<T: Serialize>(pczt_version: u32, value: &T) -> Vec<u8> {
181    let mut bytes = crate::serialize_header(REQUEST_MAGIC_BYTES, VERSION);
182    bytes.extend_from_slice(&pczt_version.to_le_bytes());
183    postcard::to_extend(value, bytes).expect("can serialize into memory")
184}
185
186fn serialize_versioned<T: Serialize>(magic: &[u8; 4], value: &T) -> Vec<u8> {
187    let bytes = crate::serialize_header(magic, VERSION);
188    postcard::to_extend(value, bytes).expect("can serialize into memory")
189}
190
191mod v1 {
192    use super::*;
193    use serde_with::serde_as;
194
195    #[derive(Deserialize, Serialize)]
196    pub(super) struct BatchSignRequest<P> {
197        pub(super) pczts: Vec<P>,
198    }
199
200    #[derive(Deserialize, Serialize)]
201    pub(super) struct BatchSignResponse {
202        pub(super) signatures: Vec<Vec<SpendAuthSignature>>,
203    }
204
205    #[derive(Deserialize, Serialize)]
206    pub(super) enum ValuePool {
207        Orchard,
208        Ironwood,
209    }
210
211    #[serde_as]
212    #[derive(Deserialize, Serialize)]
213    pub(super) struct SpendAuthSignature {
214        pub(super) value_pool: ValuePool,
215        pub(super) action_index: u32,
216        #[serde_as(as = "[_; 64]")]
217        pub(super) signature: [u8; 64],
218    }
219
220    impl TryFrom<&super::BatchSignRequest> for BatchSignRequest<crate::v2::Pczt> {
221        type Error = EncodingError;
222
223        fn try_from(request: &super::BatchSignRequest) -> Result<Self, Self::Error> {
224            Ok(Self {
225                pczts: request
226                    .pczts
227                    .iter()
228                    .cloned()
229                    .map(|pczt| {
230                        crate::v2::Pczt::try_from(pczt).map_err(EncodingError::PcztEncoding)
231                    })
232                    .collect::<Result<_, _>>()?,
233            })
234        }
235    }
236
237    impl From<BatchSignRequest<crate::v1::Pczt>> for super::BatchSignRequest {
238        fn from(request: BatchSignRequest<crate::v1::Pczt>) -> Self {
239            Self {
240                pczts: request.pczts.into_iter().map(Into::into).collect(),
241            }
242        }
243    }
244
245    impl TryFrom<BatchSignRequest<crate::v2::Pczt>> for super::BatchSignRequest {
246        type Error = ParseError;
247
248        fn try_from(request: BatchSignRequest<crate::v2::Pczt>) -> Result<Self, Self::Error> {
249            Ok(Self {
250                pczts: request
251                    .pczts
252                    .into_iter()
253                    .map(|pczt| pczt.into_logical().map_err(ParseError::PcztParse))
254                    .collect::<Result<_, _>>()?,
255            })
256        }
257    }
258
259    impl TryFrom<&super::BatchSignResponse> for BatchSignResponse {
260        type Error = EncodingError;
261
262        fn try_from(response: &super::BatchSignResponse) -> Result<Self, Self::Error> {
263            Ok(Self {
264                signatures: response
265                    .signatures
266                    .iter()
267                    .map(|signatures| {
268                        signatures
269                            .iter()
270                            .map(SpendAuthSignature::try_from)
271                            .collect::<Result<_, _>>()
272                    })
273                    .collect::<Result<_, EncodingError>>()?,
274            })
275        }
276    }
277
278    impl TryFrom<BatchSignResponse> for super::BatchSignResponse {
279        type Error = ParseError;
280
281        fn try_from(response: BatchSignResponse) -> Result<Self, Self::Error> {
282            Ok(Self {
283                signatures: response
284                    .signatures
285                    .into_iter()
286                    .map(|signatures| {
287                        signatures
288                            .into_iter()
289                            .map(super::SpendAuthSignature::try_from)
290                            .collect::<Result<_, _>>()
291                    })
292                    .collect::<Result<_, ParseError>>()?,
293            })
294        }
295    }
296
297    impl TryFrom<&super::SpendAuthSignature> for SpendAuthSignature {
298        type Error = EncodingError;
299
300        fn try_from(signature: &super::SpendAuthSignature) -> Result<Self, Self::Error> {
301            Ok(Self {
302                value_pool: match signature.value_pool() {
303                    orchard::ValuePool::Orchard => ValuePool::Orchard,
304                    orchard::ValuePool::Ironwood => ValuePool::Ironwood,
305                },
306                action_index: u32::try_from(signature.action_index())
307                    .map_err(|_| EncodingError::ActionIndexOutOfRange)?,
308                signature: *signature.signature(),
309            })
310        }
311    }
312
313    impl TryFrom<SpendAuthSignature> for super::SpendAuthSignature {
314        type Error = ParseError;
315
316        fn try_from(signature: SpendAuthSignature) -> Result<Self, Self::Error> {
317            let value_pool = match signature.value_pool {
318                ValuePool::Orchard => orchard::ValuePool::Orchard,
319                ValuePool::Ironwood => orchard::ValuePool::Ironwood,
320            };
321            let action_index = usize::try_from(signature.action_index)
322                .map_err(|_| ParseError::ActionIndexOutOfRange)?;
323            Ok(Self::from_parts(
324                value_pool,
325                action_index,
326                signature.signature,
327            ))
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use serde_with::serde_as;
336    use zcash_protocol::consensus::BranchId;
337
338    use crate::roles::creator::Creator;
339
340    fn empty_pczt(expiry_height: u32) -> Pczt {
341        Creator::new(BranchId::Nu6_3.into(), expiry_height, 133, None, None)
342            .unwrap()
343            .build()
344            .unwrap()
345    }
346
347    fn v5_pczt(expiry_height: u32) -> Pczt {
348        Creator::new(
349            BranchId::Nu6.into(),
350            expiry_height,
351            133,
352            Some([0; 32]),
353            Some([0; 32]),
354        )
355        .unwrap()
356        .build()
357        .unwrap()
358    }
359
360    #[test]
361    fn request_round_trip() {
362        let request = BatchSignRequest::new(vec![empty_pczt(10_000_000), empty_pczt(10_000_001)]);
363
364        let expected_pczts = request
365            .pczts()
366            .iter()
367            .cloned()
368            .map(|pczt| pczt.serialize().unwrap())
369            .collect::<Vec<_>>();
370        let expected_body = postcard::to_allocvec(
371            &v1::BatchSignRequest::<crate::v2::Pczt>::try_from(&request).unwrap(),
372        )
373        .unwrap();
374        let encoded = request.serialize().unwrap();
375        let decoded = BatchSignRequest::parse(&encoded).unwrap();
376
377        assert_eq!(&encoded[..4], REQUEST_MAGIC_BYTES);
378        assert_eq!(
379            u32::from_le_bytes(encoded[4..8].try_into().unwrap()),
380            VERSION
381        );
382        assert_eq!(
383            u32::from_le_bytes(encoded[8..12].try_into().unwrap()),
384            crate::PCZT_VERSION_2
385        );
386        assert_eq!(&encoded[12..], expected_body);
387        assert_eq!(decoded.pczts().len(), expected_pczts.len());
388        for (decoded, expected) in decoded.pczts().iter().zip(expected_pczts) {
389            assert_eq!(decoded.clone().serialize().unwrap(), expected);
390        }
391    }
392
393    #[test]
394    fn request_parse_accepts_v1_pczt_payloads() {
395        let pczt = v5_pczt(10_000_000);
396        let expected = crate::v1::Pczt::try_from(pczt.clone()).unwrap().serialize();
397        let wire = v1::BatchSignRequest {
398            pczts: vec![crate::v1::Pczt::try_from(pczt).unwrap()],
399        };
400
401        let encoded = serialize_request(crate::PCZT_VERSION_1, &wire);
402        let decoded = BatchSignRequest::parse(&encoded).unwrap();
403
404        assert_eq!(decoded.pczts().len(), 1);
405        assert_eq!(
406            crate::v1::Pczt::try_from(decoded.pczts()[0].clone())
407                .unwrap()
408                .serialize(),
409            expected
410        );
411    }
412
413    #[test]
414    fn request_parse_rejects_invalid_body() {
415        let mut encoded = crate::serialize_header(REQUEST_MAGIC_BYTES, VERSION);
416        encoded.extend_from_slice(&crate::PCZT_VERSION_2.to_le_bytes());
417        encoded.push(1);
418
419        assert!(matches!(
420            BatchSignRequest::parse(&encoded),
421            Err(ParseError::Invalid(_))
422        ));
423    }
424
425    #[test]
426    fn response_round_trip() {
427        let response = BatchSignResponse::new(vec![
428            vec![SpendAuthSignature::from_parts(
429                orchard::ValuePool::Orchard,
430                0,
431                [0x11; 64],
432            )],
433            vec![SpendAuthSignature::from_parts(
434                orchard::ValuePool::Ironwood,
435                12,
436                [0x22; 64],
437            )],
438        ]);
439
440        let encoded = response.serialize().unwrap();
441
442        assert_eq!(&encoded[..4], RESPONSE_MAGIC_BYTES);
443        assert_eq!(
444            u32::from_le_bytes(encoded[4..8].try_into().unwrap()),
445            VERSION
446        );
447        assert_eq!(BatchSignResponse::parse(&encoded).unwrap(), response);
448    }
449
450    #[test]
451    fn parse_rejects_unknown_version() {
452        let encoded = crate::serialize_header(REQUEST_MAGIC_BYTES, VERSION + 1);
453        assert!(matches!(
454            BatchSignRequest::parse(&encoded),
455            Err(ParseError::UnknownVersion(2))
456        ));
457    }
458
459    #[test]
460    fn request_parse_rejects_unknown_pczt_version_before_parsing_body() {
461        let mut encoded = crate::serialize_header(REQUEST_MAGIC_BYTES, VERSION);
462        encoded.extend_from_slice(&(crate::PCZT_VERSION_2 + 1).to_le_bytes());
463        encoded.extend_from_slice(b"not a Postcard body");
464
465        assert!(matches!(
466            BatchSignRequest::parse(&encoded),
467            Err(ParseError::UnknownPcztVersion(3))
468        ));
469    }
470
471    #[test]
472    fn request_and_response_use_distinct_magic_bytes() {
473        let request = BatchSignRequest::new(vec![]).serialize().unwrap();
474        let response = BatchSignResponse::new(vec![]).serialize().unwrap();
475
476        assert!(matches!(
477            BatchSignResponse::parse(&request),
478            Err(ParseError::InvalidMagic)
479        ));
480        assert!(matches!(
481            BatchSignRequest::parse(&response),
482            Err(ParseError::InvalidMagic)
483        ));
484    }
485
486    #[test]
487    fn parse_rejects_short_header() {
488        assert!(matches!(
489            BatchSignRequest::parse(&[0; 7]),
490            Err(ParseError::TooShort)
491        ));
492    }
493
494    #[test]
495    fn parse_rejects_trailing_data() {
496        let mut encoded = BatchSignRequest::new(vec![]).serialize().unwrap();
497        encoded.push(0);
498        assert!(matches!(
499            BatchSignRequest::parse(&encoded),
500            Err(ParseError::TrailingData)
501        ));
502    }
503
504    #[test]
505    fn response_parse_rejects_unknown_value_pool() {
506        #[derive(Serialize)]
507        struct RawBatchSignResponse {
508            signatures: Vec<Vec<RawSpendAuthSignature>>,
509        }
510
511        #[serde_as]
512        #[derive(Serialize)]
513        struct RawSpendAuthSignature {
514            value_pool: u8,
515            action_index: u32,
516            #[serde_as(as = "[_; 64]")]
517            signature: [u8; 64],
518        }
519
520        let wire = RawBatchSignResponse {
521            signatures: vec![vec![RawSpendAuthSignature {
522                value_pool: 2,
523                action_index: 0,
524                signature: [0; 64],
525            }]],
526        };
527        let encoded = serialize_versioned(RESPONSE_MAGIC_BYTES, &wire);
528        assert!(matches!(
529            BatchSignResponse::parse(&encoded),
530            Err(ParseError::Invalid(_))
531        ));
532    }
533}