Skip to main content

pczt/roles/updater/
mod.rs

1//! The Updater role (anyone can contribute).
2//!
3//! - Adds information necessary for subsequent entities to proceed, such as key paths
4//!   for signing spends.
5
6use alloc::{string::String, vec::Vec};
7
8use crate::{Pczt, common::Global};
9
10#[cfg(all(
11    any(feature = "sapling", feature = "orchard"),
12    zcash_unstable = "nutachyon"
13))]
14use zcash_protocol::constants::{V7_TX_VERSION, V7_VERSION_GROUP_ID};
15#[cfg(any(feature = "sapling", feature = "orchard"))]
16use zcash_protocol::{
17    consensus::BranchId,
18    constants::{V6_TX_VERSION, V6_VERSION_GROUP_ID},
19};
20
21#[cfg(feature = "orchard")]
22mod orchard;
23#[cfg(feature = "orchard")]
24pub use orchard::OrchardError;
25
26#[cfg(feature = "sapling")]
27mod sapling;
28#[cfg(feature = "sapling")]
29pub use sapling::SaplingError;
30
31#[cfg(feature = "transparent")]
32mod transparent;
33#[cfg(feature = "transparent")]
34pub use transparent::TransparentError;
35
36pub struct Updater {
37    pczt: Pczt,
38}
39
40impl Updater {
41    /// Instantiates the Updater role with the given PCZT.
42    pub fn new(pczt: Pczt) -> Self {
43        Self { pczt }
44    }
45
46    /// Updates the global transaction details with information in the given closure.
47    pub fn update_global_with<F>(self, f: F) -> Self
48    where
49        F: FnOnce(GlobalUpdater<'_>),
50    {
51        let Pczt {
52            mut global,
53            transparent,
54            sapling,
55            orchard,
56            ironwood,
57        } = self.pczt;
58
59        f(GlobalUpdater(&mut global));
60
61        Self {
62            pczt: Pczt {
63                global,
64                transparent,
65                sapling,
66                orchard,
67                ironwood,
68            },
69        }
70    }
71
72    /// Sets the Sapling bundle anchor.
73    ///
74    /// This may be called after shielded signatures have been added for
75    /// transaction formats that do not commit shielded signatures to anchors.
76    /// Sapling spend proofs depend on the anchor, so this must be called before
77    /// proof creation for Sapling spends.
78    ///
79    /// Returns an error if the PCZT's transaction format does not support this
80    /// update, if any Sapling spend proof is already present, or if the PCZT
81    /// already contains a different Sapling anchor.
82    #[cfg(feature = "sapling")]
83    pub fn set_sapling_anchor(
84        mut self,
85        anchor: ::sapling::Anchor,
86    ) -> Result<Self, AnchorUpdateError> {
87        ensure_anchor_update_supported(&self.pczt.global)?;
88        ensure_no_sapling_spend_proof(&self.pczt.sapling)?;
89        set_anchor(&mut self.pczt.sapling.anchor, anchor.to_bytes())?;
90        Ok(self)
91    }
92
93    /// Sets the Orchard bundle anchor.
94    ///
95    /// This may be called after shielded signatures have been added for
96    /// transaction formats that do not commit shielded signatures to anchors.
97    /// Orchard proofs depend on the anchor, so this must be called before proof
98    /// creation.
99    ///
100    /// Returns an error if the PCZT's transaction format does not support this
101    /// update, if an Orchard proof is already present, or if the PCZT already
102    /// contains a different Orchard anchor.
103    #[cfg(feature = "orchard")]
104    pub fn set_orchard_anchor(
105        mut self,
106        anchor: ::orchard::Anchor,
107    ) -> Result<Self, AnchorUpdateError> {
108        ensure_anchor_update_supported(&self.pczt.global)?;
109        ensure_no_orchard_proof_for_anchor(&self.pczt.orchard)?;
110        set_anchor(&mut self.pczt.orchard.anchor, anchor.to_bytes())?;
111        Ok(self)
112    }
113
114    /// Sets the Ironwood bundle anchor.
115    ///
116    /// This may be called after shielded signatures have been added for
117    /// transaction formats that do not commit shielded signatures to anchors.
118    /// Ironwood proofs depend on the anchor, so this must be called before proof
119    /// creation.
120    ///
121    /// Returns an error if the PCZT's transaction format does not support this
122    /// update, if an Ironwood proof is already present, or if the PCZT already
123    /// contains a different Ironwood anchor.
124    #[cfg(feature = "orchard")]
125    pub fn set_ironwood_anchor(
126        mut self,
127        anchor: ::orchard::Anchor,
128    ) -> Result<Self, AnchorUpdateError> {
129        ensure_anchor_update_supported(&self.pczt.global)?;
130        ensure_no_orchard_proof_for_anchor(&self.pczt.ironwood)?;
131        set_anchor(&mut self.pczt.ironwood.anchor, anchor.to_bytes())?;
132        Ok(self)
133    }
134
135    /// Sets spend witnesses for Sapling spends by spend index.
136    ///
137    /// This may be called after shielded signatures have been added. Sapling
138    /// spend proofs depend on the witnesses, so this must be called before proof
139    /// creation for the updated spends.
140    ///
141    /// Returns an error if any witness references a spend index that does not
142    /// exist, or if a target spend proof is already present.
143    #[cfg(feature = "sapling")]
144    pub fn set_sapling_spend_witnesses(
145        mut self,
146        witnesses: impl IntoIterator<Item = (usize, ::sapling::MerklePath)>,
147    ) -> Result<Self, SpendWitnessUpdateError> {
148        set_sapling_spend_witnesses(&mut self.pczt.sapling.spends, witnesses)?;
149
150        Ok(self)
151    }
152
153    /// Sets spend witnesses for Orchard actions by action index.
154    ///
155    /// This may be called after shielded signatures have been added. Orchard
156    /// proofs depend on the witnesses, so this must be called before proof
157    /// creation.
158    ///
159    /// Returns an error if any witness references an action index that does not
160    /// exist, if the Orchard bundle is not using Orchard note plaintexts, or if
161    /// an Orchard proof is already present.
162    #[cfg(feature = "orchard")]
163    pub fn set_orchard_spend_witnesses(
164        mut self,
165        witnesses: impl IntoIterator<Item = (usize, ::orchard::tree::MerklePath)>,
166    ) -> Result<Self, SpendWitnessUpdateError> {
167        if self.pczt.orchard.note_version != crate::orchard::NoteVersion::V2 {
168            return Err(SpendWitnessUpdateError::UnexpectedNoteVersion);
169        }
170        ensure_no_orchard_proof_for_witness(&self.pczt.orchard)?;
171        set_orchard_spend_witnesses(&mut self.pczt.orchard.actions, witnesses)?;
172
173        Ok(self)
174    }
175
176    /// Sets spend witnesses for Ironwood actions by action index.
177    ///
178    /// This may be called after shielded signatures have been added. Ironwood
179    /// proofs depend on the witnesses, so this must be called before proof
180    /// creation.
181    ///
182    /// Returns an error if any witness references an action index that does not
183    /// exist, if the Ironwood bundle is not using Ironwood note plaintexts, or if
184    /// an Ironwood proof is already present.
185    #[cfg(feature = "orchard")]
186    pub fn set_ironwood_spend_witnesses(
187        mut self,
188        witnesses: impl IntoIterator<Item = (usize, ::orchard::tree::MerklePath)>,
189    ) -> Result<Self, SpendWitnessUpdateError> {
190        if self.pczt.ironwood.note_version != crate::orchard::NoteVersion::V3 {
191            return Err(SpendWitnessUpdateError::UnexpectedNoteVersion);
192        }
193        ensure_no_orchard_proof_for_witness(&self.pczt.ironwood)?;
194        set_orchard_spend_witnesses(&mut self.pczt.ironwood.actions, witnesses)?;
195
196        Ok(self)
197    }
198
199    /// Finishes the Updater role, returning the updated PCZT.
200    pub fn finish(self) -> Pczt {
201        self.pczt
202    }
203}
204
205#[cfg(feature = "sapling")]
206fn ensure_no_sapling_spend_proof(bundle: &crate::sapling::Bundle) -> Result<(), AnchorUpdateError> {
207    if bundle.spends.iter().any(|spend| spend.zkproof.is_some()) {
208        Err(AnchorUpdateError::ProofAlreadyPresent)
209    } else {
210        Ok(())
211    }
212}
213
214#[cfg(feature = "orchard")]
215fn ensure_no_orchard_proof_for_anchor(
216    bundle: &crate::orchard::Bundle,
217) -> Result<(), AnchorUpdateError> {
218    if bundle.zkproof.is_some() {
219        Err(AnchorUpdateError::ProofAlreadyPresent)
220    } else {
221        Ok(())
222    }
223}
224
225#[cfg(feature = "orchard")]
226fn ensure_no_orchard_proof_for_witness(
227    bundle: &crate::orchard::Bundle,
228) -> Result<(), SpendWitnessUpdateError> {
229    if bundle.zkproof.is_some() {
230        Err(SpendWitnessUpdateError::ProofAlreadyPresent)
231    } else {
232        Ok(())
233    }
234}
235
236#[cfg(any(feature = "sapling", feature = "orchard"))]
237fn ensure_anchor_update_supported(global: &Global) -> Result<(), AnchorUpdateError> {
238    let supported_format = match (global.tx_version, global.version_group_id) {
239        (V6_TX_VERSION, V6_VERSION_GROUP_ID) => true,
240        #[cfg(zcash_unstable = "nutachyon")]
241        (V7_TX_VERSION, V7_VERSION_GROUP_ID) => true,
242        _ => false,
243    };
244    if !supported_format {
245        return Err(AnchorUpdateError::UnsupportedTransactionFormat);
246    }
247
248    match BranchId::try_from(global.consensus_branch_id) {
249        Ok(BranchId::Nu6_3) => Ok(()),
250        Ok(BranchId::Nu7) => Ok(()),
251        #[cfg(zcash_unstable = "nutachyon")]
252        Ok(BranchId::NuTachyon) => Ok(()),
253        Ok(_) => Err(AnchorUpdateError::UnsupportedConsensusBranchId),
254        Err(_) => Err(AnchorUpdateError::UnknownConsensusBranchId),
255    }
256}
257
258#[cfg(any(feature = "sapling", feature = "orchard"))]
259fn set_anchor(slot: &mut Option<[u8; 32]>, anchor: [u8; 32]) -> Result<(), AnchorUpdateError> {
260    match slot {
261        Some(existing) if *existing != anchor => Err(AnchorUpdateError::ConflictingAnchor),
262        _ => {
263            *slot = Some(anchor);
264            Ok(())
265        }
266    }
267}
268
269#[cfg(feature = "sapling")]
270fn set_sapling_spend_witnesses(
271    spends: &mut [crate::sapling::Spend],
272    witnesses: impl IntoIterator<Item = (usize, ::sapling::MerklePath)>,
273) -> Result<(), SpendWitnessUpdateError> {
274    for (spend_index, merkle_path) in witnesses {
275        let spend = spends
276            .get_mut(spend_index)
277            .ok_or(SpendWitnessUpdateError::InvalidSpendIndex(spend_index))?;
278        if spend.zkproof.is_some() {
279            return Err(SpendWitnessUpdateError::ProofAlreadyPresent);
280        }
281        let position = u32::try_from(u64::from(merkle_path.position()))
282            .map_err(|_| SpendWitnessUpdateError::PositionOutOfRange)?;
283        let mut auth_path = [[0; 32]; 32];
284        for (target, node) in auth_path.iter_mut().zip(merkle_path.path_elems()) {
285            *target = node.to_bytes();
286        }
287        spend.witness = Some((position, auth_path));
288    }
289
290    Ok(())
291}
292
293#[cfg(feature = "orchard")]
294fn set_orchard_spend_witnesses(
295    actions: &mut [crate::orchard::Action],
296    witnesses: impl IntoIterator<Item = (usize, ::orchard::tree::MerklePath)>,
297) -> Result<(), SpendWitnessUpdateError> {
298    for (action_index, merkle_path) in witnesses {
299        let action = actions
300            .get_mut(action_index)
301            .ok_or(SpendWitnessUpdateError::InvalidSpendIndex(action_index))?;
302        action.spend.witness = Some((
303            merkle_path.position(),
304            merkle_path.auth_path().map(|node| node.to_bytes()),
305        ));
306    }
307
308    Ok(())
309}
310
311/// An updater for a transparent PCZT output.
312pub struct GlobalUpdater<'a>(&'a mut Global);
313
314impl GlobalUpdater<'_> {
315    /// Stores the given proprietary value at the given key.
316    pub fn set_proprietary(&mut self, key: String, value: Vec<u8>) {
317        self.0.proprietary.insert(key, value);
318    }
319}
320
321/// Errors that can occur while setting Sapling, Orchard, or Ironwood anchors.
322#[cfg(any(feature = "sapling", feature = "orchard"))]
323#[derive(Debug, PartialEq, Eq)]
324#[non_exhaustive]
325pub enum AnchorUpdateError {
326    /// The PCZT transaction format does not support this update.
327    UnsupportedTransactionFormat,
328    /// The PCZT's consensus branch ID is unrecognized.
329    UnknownConsensusBranchId,
330    /// The PCZT's consensus branch ID does not support this update.
331    UnsupportedConsensusBranchId,
332    /// The bundle already contains a proof that depends on the current anchor.
333    ProofAlreadyPresent,
334    /// The bundle already contains a different anchor.
335    ConflictingAnchor,
336}
337
338#[cfg(any(feature = "sapling", feature = "orchard"))]
339impl core::fmt::Display for AnchorUpdateError {
340    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341        match self {
342            AnchorUpdateError::UnsupportedTransactionFormat => {
343                write!(
344                    f,
345                    "PCZT transaction format does not support shielded anchor updates"
346                )
347            }
348            AnchorUpdateError::UnknownConsensusBranchId => {
349                write!(f, "unknown consensus branch ID")
350            }
351            AnchorUpdateError::UnsupportedConsensusBranchId => {
352                write!(
353                    f,
354                    "consensus branch ID does not support shielded anchor updates"
355                )
356            }
357            AnchorUpdateError::ProofAlreadyPresent => {
358                write!(
359                    f,
360                    "shielded proof that depends on the anchor is already present"
361                )
362            }
363            AnchorUpdateError::ConflictingAnchor => {
364                write!(f, "bundle already contains a different anchor")
365            }
366        }
367    }
368}
369
370/// Errors that can occur while setting Sapling, Orchard, or Ironwood spend witnesses.
371#[cfg(any(feature = "sapling", feature = "orchard"))]
372#[derive(Debug, PartialEq, Eq)]
373#[non_exhaustive]
374pub enum SpendWitnessUpdateError {
375    /// The requested Sapling spend index or Orchard/Ironwood action index does not exist.
376    InvalidSpendIndex(usize),
377    /// The bundle already contains a proof that depends on the current witness.
378    ProofAlreadyPresent,
379    /// The witness position cannot be represented in the PCZT wire format.
380    PositionOutOfRange,
381    /// The bundle's note-plaintext version does not match the pool being updated.
382    UnexpectedNoteVersion,
383}
384
385#[cfg(any(feature = "sapling", feature = "orchard"))]
386impl core::fmt::Display for SpendWitnessUpdateError {
387    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
388        match self {
389            SpendWitnessUpdateError::InvalidSpendIndex(index) => {
390                write!(f, "spend index {index} does not exist")
391            }
392            SpendWitnessUpdateError::ProofAlreadyPresent => {
393                write!(f, "proof for target spend or bundle is already present")
394            }
395            SpendWitnessUpdateError::PositionOutOfRange => {
396                write!(
397                    f,
398                    "witness position cannot be represented in the PCZT wire format"
399                )
400            }
401            SpendWitnessUpdateError::UnexpectedNoteVersion => {
402                write!(
403                    f,
404                    "bundle note-plaintext version does not match the pool being updated"
405                )
406            }
407        }
408    }
409}