Skip to main content

pczt/roles/low_level_signer/
mod.rs

1//! A low-level variant of the Signer role, for dependency-constrained environments.
2
3use crate::Pczt;
4
5pub struct Signer {
6    pczt: Pczt,
7}
8
9impl Signer {
10    /// Instantiates the low-level Signer role with the given PCZT.
11    pub fn new(pczt: Pczt) -> Self {
12        Self { pczt }
13    }
14
15    /// Exposes the capability to sign the Ironwood spends.
16    ///
17    /// The bundle is parsed with a preverified signing parse that skips deriving each
18    /// spend's `FullViewingKey` (an expensive step the spend authorization signature
19    /// does not depend on). Callers that rely on the wire `fvk` bytes MUST have
20    /// already run the full Verifier checks over the identical PCZT bytes: they are
21    /// not validated here, and the signing closure sees each spend's `fvk` as `None`.
22    ///
23    /// The signing closure must not add, remove, or reorder actions. A well-behaved
24    /// closure leaves the returned PCZT's wire `fvk` bytes unchanged; a violating one
25    /// is detected and returns [`OrchardParseError::SigningClosureModifiedActions`],
26    /// leaving the PCZT unmodified.
27    #[cfg(feature = "orchard")]
28    pub fn sign_ironwood_with<E, F>(self, f: F) -> Result<Self, E>
29    where
30        E: From<OrchardParseError>,
31        F: FnOnce(&Pczt, &mut orchard::pczt::Bundle, &mut u8) -> Result<(), E>,
32    {
33        let mut pczt = self.pczt;
34
35        let mut tx_modifiable = pczt.global.tx_modifiable;
36        let anchor_requirement =
37            crate::common::AnchorRequirement::for_pre_authorization(pczt.global.tx_version);
38
39        let fvk_snapshot = snapshot_spend_fvks(&pczt.ironwood);
40        let mut parsed = pczt
41            .ironwood
42            .clone()
43            .into_ironwood_parsed_preverified_for_signing(anchor_requirement)
44            .map_err(OrchardParseError::Parse)?;
45
46        f(&pczt, &mut parsed.bundle, &mut tx_modifiable)?;
47
48        pczt.global.tx_modifiable = tx_modifiable;
49        pczt.ironwood = parsed.reserialize();
50        restore_spend_fvks(&mut pczt.ironwood, &fvk_snapshot).map_err(E::from)?;
51
52        Ok(Self { pczt })
53    }
54
55    /// Exposes the capability to sign the Orchard spends.
56    ///
57    /// The bundle is parsed with a preverified signing parse that skips deriving each
58    /// spend's `FullViewingKey` (an expensive step the spend authorization signature
59    /// does not depend on). Callers that rely on the wire `fvk` bytes MUST have
60    /// already run the full Verifier checks over the identical PCZT bytes: they are
61    /// not validated here, and the signing closure sees each spend's `fvk` as `None`.
62    ///
63    /// The signing closure must not add, remove, or reorder actions. A well-behaved
64    /// closure leaves the returned PCZT's wire `fvk` bytes unchanged; a violating one
65    /// is detected and returns [`OrchardParseError::SigningClosureModifiedActions`],
66    /// leaving the PCZT unmodified.
67    #[cfg(feature = "orchard")]
68    pub fn sign_orchard_with<E, F>(self, f: F) -> Result<Self, E>
69    where
70        E: From<OrchardParseError>,
71        F: FnOnce(&Pczt, &mut orchard::pczt::Bundle, &mut u8) -> Result<(), E>,
72    {
73        let mut pczt = self.pczt;
74
75        let mut tx_modifiable = pczt.global.tx_modifiable;
76        let anchor_requirement =
77            crate::common::AnchorRequirement::for_pre_authorization(pczt.global.tx_version);
78
79        let bundle_version = crate::orchard::orchard_bundle_version(&pczt.global)
80            .ok_or(OrchardParseError::UnsupportedConsensusBranchId)?;
81        let fvk_snapshot = snapshot_spend_fvks(&pczt.orchard);
82        let mut parsed = pczt
83            .orchard
84            .clone()
85            .into_parsed_with_version_preverified_for_signing(bundle_version, anchor_requirement)
86            .map_err(OrchardParseError::Parse)?;
87
88        f(&pczt, &mut parsed.bundle, &mut tx_modifiable)?;
89
90        pczt.global.tx_modifiable = tx_modifiable;
91        pczt.orchard = parsed.reserialize();
92        restore_spend_fvks(&mut pczt.orchard, &fvk_snapshot).map_err(E::from)?;
93
94        Ok(Self { pczt })
95    }
96
97    /// Exposes the capability to sign the Sapling spends.
98    #[cfg(feature = "sapling")]
99    pub fn sign_sapling_with<E, F>(self, f: F) -> Result<Self, E>
100    where
101        E: From<crate::sapling::ParseError>,
102        F: FnOnce(&Pczt, &mut sapling::pczt::Bundle, &mut u8) -> Result<(), E>,
103    {
104        let mut pczt = self.pczt;
105
106        let mut tx_modifiable = pczt.global.tx_modifiable;
107        let anchor_requirement =
108            crate::common::AnchorRequirement::for_pre_authorization(pczt.global.tx_version);
109
110        let mut parsed = pczt.sapling.clone().into_parsed(anchor_requirement)?;
111
112        f(&pczt, &mut parsed.bundle, &mut tx_modifiable)?;
113
114        pczt.global.tx_modifiable = tx_modifiable;
115        pczt.sapling = parsed.reserialize();
116
117        Ok(Self { pczt })
118    }
119
120    /// Exposes the capability to sign the transparent spends.
121    #[cfg(feature = "transparent")]
122    pub fn sign_transparent_with<E, F>(self, f: F) -> Result<Self, E>
123    where
124        E: From<transparent::pczt::ParseError>,
125        F: FnOnce(&Pczt, &mut transparent::pczt::Bundle, &mut u8) -> Result<(), E>,
126    {
127        let mut pczt = self.pczt;
128
129        let mut tx_modifiable = pczt.global.tx_modifiable;
130
131        let mut bundle = pczt.transparent.clone().into_parsed()?;
132
133        f(&pczt, &mut bundle, &mut tx_modifiable)?;
134
135        pczt.global.tx_modifiable = tx_modifiable;
136        pczt.transparent = crate::transparent::Bundle::serialize_from(bundle);
137
138        Ok(Self { pczt })
139    }
140
141    /// Finishes the low-level Signer role, returning the updated PCZT.
142    pub fn finish(self) -> Pczt {
143        self.pczt
144    }
145}
146
147/// A by-position snapshot of each spend's wire `(rk, fvk)` bytes: the `fvk` that the
148/// preverified signing parse drops and must restore, paired with the `rk` used as a
149/// per-position tamper check. See [`restore_spend_fvks`].
150#[cfg(feature = "orchard")]
151type SpendFvkSnapshot = alloc::vec::Vec<([u8; 32], Option<[u8; 96]>)>;
152
153/// Snapshots each action's spend `(rk, fvk)` wire bytes, by position, for
154/// [`restore_spend_fvks`] to restore after serialization.
155#[cfg(feature = "orchard")]
156fn snapshot_spend_fvks(bundle: &crate::orchard::Bundle) -> SpendFvkSnapshot {
157    bundle
158        .actions()
159        .iter()
160        .map(|action| (action.spend.rk, action.spend.fvk))
161        .collect()
162}
163
164/// Restores the wire `fvk` bytes from [`snapshot_spend_fvks`] into each spend by
165/// position, after checking the signing closure did not resize or reorder the action
166/// list.
167///
168/// Positional restore is only sound if each position still holds its original action,
169/// so this checks the action count and each position's wire `rk` — the one
170/// always-present spend field that pins an action's identity, since a nullifier can
171/// be shared — against the snapshot before writing any `fvk`. On a mismatch it writes
172/// nothing and returns [`OrchardParseError::SigningClosureModifiedActions`].
173#[cfg(feature = "orchard")]
174fn restore_spend_fvks(
175    bundle: &mut crate::orchard::Bundle,
176    snapshot: &SpendFvkSnapshot,
177) -> Result<(), OrchardParseError> {
178    // Reject a resized or reordered list before writing: a count change misaligns
179    // every later `fvk`, and a reorder moves `rk`s off their snapshotted positions.
180    if bundle.actions.len() != snapshot.len() {
181        return Err(OrchardParseError::SigningClosureModifiedActions);
182    }
183    for (action, (rk, _)) in bundle.actions.iter().zip(snapshot) {
184        if action.spend.rk != *rk {
185            return Err(OrchardParseError::SigningClosureModifiedActions);
186        }
187    }
188    for (action, (_, fvk)) in bundle.actions.iter_mut().zip(snapshot) {
189        action.spend.fvk = *fvk;
190    }
191    Ok(())
192}
193
194/// Errors that can occur while parsing an Orchard-protocol bundle of a PCZT for
195/// signing.
196#[cfg(feature = "orchard")]
197#[derive(Debug)]
198pub enum OrchardParseError {
199    /// The bundle data was structurally invalid.
200    Parse(crate::orchard::ParseError),
201    /// The PCZT's consensus branch ID is unrecognized, or predates NU5 (under which
202    /// the Orchard protocol is not supported).
203    UnsupportedConsensusBranchId,
204    /// A signing closure passed to [`Signer::sign_orchard_with`] or
205    /// [`Signer::sign_ironwood_with`] added, removed, or reordered actions, which
206    /// those methods forbid. The PCZT is left unmodified.
207    SigningClosureModifiedActions,
208}
209
210#[cfg(feature = "orchard")]
211impl From<crate::orchard::ParseError> for OrchardParseError {
212    fn from(e: crate::orchard::ParseError) -> Self {
213        OrchardParseError::Parse(e)
214    }
215}
216
217#[cfg(all(test, feature = "orchard"))]
218mod tests {
219    use alloc::collections::BTreeMap;
220    use alloc::string::String;
221    use alloc::vec::Vec;
222
223    use crate::orchard::{Action, Bundle, EncCiphertext, NoteVersion, Output, Spend};
224
225    use super::{OrchardParseError, restore_spend_fvks, snapshot_spend_fvks};
226
227    #[test]
228    fn restore_spend_fvks_preserves_duplicate_nullifiers_by_position() {
229        let first_fvk = Some([7u8; 96]);
230        let second_fvk = Some([9u8; 96]);
231        // Shared nullifier, distinct `rk`s: proves the restore keys on position, not
232        // the (here ambiguous) nullifier.
233        let mut bundle = bundle_with_duplicate_nullifier_fvks([first_fvk, second_fvk]);
234        let snapshot = snapshot_spend_fvks(&bundle);
235
236        bundle.actions[0].spend.fvk = None;
237        bundle.actions[1].spend.fvk = None;
238
239        restore_spend_fvks(&mut bundle, &snapshot).expect("actions were not modified");
240
241        assert_eq!(bundle.actions[0].spend.fvk, first_fvk);
242        assert_eq!(bundle.actions[1].spend.fvk, second_fvk);
243    }
244
245    #[test]
246    fn restore_spend_fvks_rejects_reordered_actions() {
247        let first_fvk = Some([7u8; 96]);
248        let second_fvk = Some([9u8; 96]);
249        let mut bundle = bundle_with_duplicate_nullifier_fvks([first_fvk, second_fvk]);
250        let snapshot = snapshot_spend_fvks(&bundle);
251
252        // A signing closure swaps the two actions; their distinct wire `rk`s move too.
253        bundle.actions.swap(0, 1);
254
255        assert!(matches!(
256            restore_spend_fvks(&mut bundle, &snapshot),
257            Err(OrchardParseError::SigningClosureModifiedActions)
258        ));
259        // Left as the closure left it: `fvk`s were not restored onto swapped actions.
260        assert_eq!(bundle.actions[0].spend.fvk, second_fvk);
261        assert_eq!(bundle.actions[1].spend.fvk, first_fvk);
262    }
263
264    /// Returns a two-action bundle whose spends share a nullifier but carry
265    /// distinct wire `rk`s and the given distinct `fvk`s.
266    fn bundle_with_duplicate_nullifier_fvks(fvks: [Option<[u8; 96]>; 2]) -> Bundle {
267        Bundle {
268            actions: fvks
269                .into_iter()
270                .enumerate()
271                // Distinct `rk` per action (`[10; 32]`, `[11; 32]`), shared
272                // nullifier `[3; 32]`.
273                .map(|(i, fvk)| Action {
274                    cv_net: Some([0; 32]),
275                    spend: Spend {
276                        nullifier: [3u8; 32],
277                        rk: [10 + i as u8; 32],
278                        spend_auth_sig: None,
279                        recipient: None,
280                        value: None,
281                        rho: None,
282                        rseed: None,
283                        fvk,
284                        witness: None,
285                        alpha: None,
286                        zip32_derivation: None,
287                        dummy_sk: None,
288                        proprietary: BTreeMap::new(),
289                    },
290                    output: Output {
291                        cmx: Some([0; 32]),
292                        ephemeral_key: [0; 32],
293                        enc_ciphertext: EncCiphertext::Encrypted(Vec::new()),
294                        out_ciphertext: Vec::new(),
295                        recipient: None,
296                        value: None,
297                        rseed: None,
298                        ock: None,
299                        zip32_derivation: None,
300                        user_address: Option::<String>::None,
301                        proprietary: BTreeMap::new(),
302                    },
303                    rcv: None,
304                })
305                .collect(),
306            flags: 0,
307            value_sum: (0, false),
308            anchor: Some([0; 32]),
309            note_version: NoteVersion::V2,
310            zkproof: None,
311            bsk: None,
312        }
313    }
314}