1use alloc::collections::BTreeMap;
6
7use crate::{
8 Pczt,
9 common::{
10 FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE,
11 FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE,
12 },
13 orchard::{Bundle as OrchardBundle, ORCHARD_SPENDS_AND_OUTPUTS_ENABLED},
14};
15
16#[cfg(feature = "zcp-builder")]
17use {
18 crate::common::FLAG_HAS_SIGHASH_SINGLE,
19 ::transparent::sighash::{SIGHASH_ANYONECANPAY, SIGHASH_SINGLE},
20 zcash_protocol::{consensus::NetworkConstants, constants::V4_TX_VERSION},
21};
22
23#[cfg(feature = "orchard")]
24use crate::orchard::bundle_version_for_revision;
25
26#[cfg(zcash_unstable = "nutachyon")]
27use zcash_protocol::constants::{V7_TX_VERSION, V7_VERSION_GROUP_ID};
28use zcash_protocol::{
29 consensus::BranchId,
30 constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID, V6_TX_VERSION, V6_VERSION_GROUP_ID},
31};
32
33const INITIAL_TX_MODIFIABLE: u8 = FLAG_TRANSPARENT_INPUTS_MODIFIABLE
35 | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE
36 | FLAG_SHIELDED_MODIFIABLE;
37
38#[derive(Debug)]
40pub enum Error {
41 AnchorRequiredForV5,
45 IronwoodNotSupported,
48 UnknownConsensusBranchId,
50 UnsupportedConsensusBranchId,
53 #[cfg(feature = "orchard")]
56 UnrepresentableOrchardFlags,
57}
58
59fn consensus_branch_id_for_pczt(consensus_branch_id: u32) -> Result<BranchId, Error> {
62 match BranchId::try_from(consensus_branch_id).map_err(|_| Error::UnknownConsensusBranchId)? {
63 BranchId::Sprout
64 | BranchId::Overwinter
65 | BranchId::Sapling
66 | BranchId::Blossom
67 | BranchId::Heartwood
68 | BranchId::Canopy => Err(Error::UnsupportedConsensusBranchId),
69 branch_id => Ok(branch_id),
70 }
71}
72
73pub struct Creator {
74 tx_version: u32,
75 version_group_id: u32,
76 consensus_branch_id: BranchId,
77 fallback_lock_time: Option<u32>,
78 expiry_height: u32,
79 coin_type: u32,
80 orchard_flags: u8,
81 ironwood_flags: u8,
82 sapling_anchor: Option<[u8; 32]>,
83 orchard_anchor: Option<[u8; 32]>,
84 ironwood_anchor: Option<[u8; 32]>,
85}
86
87impl Creator {
88 pub fn new(
102 consensus_branch_id: u32,
103 expiry_height: u32,
104 coin_type: u32,
105 sapling_anchor: Option<[u8; 32]>,
106 orchard_anchor: Option<[u8; 32]>,
107 ) -> Result<Self, Error> {
108 let branch_id = consensus_branch_id_for_pczt(consensus_branch_id)?;
109
110 let (tx_version, version_group_id) = match branch_id {
111 BranchId::Sprout
114 | BranchId::Overwinter
115 | BranchId::Sapling
116 | BranchId::Blossom
117 | BranchId::Heartwood
118 | BranchId::Canopy
119 | BranchId::Nu5
120 | BranchId::Nu6
121 | BranchId::Nu6_1
122 | BranchId::Nu6_2 => (V5_TX_VERSION, V5_VERSION_GROUP_ID),
123 BranchId::Nu6_3 => (V6_TX_VERSION, V6_VERSION_GROUP_ID),
124 BranchId::Nu7 => (V6_TX_VERSION, V6_VERSION_GROUP_ID),
125 #[cfg(zcash_unstable = "nutachyon")]
126 BranchId::NuTachyon => (V7_TX_VERSION, V7_VERSION_GROUP_ID),
127 };
128
129 Ok(Self {
130 tx_version,
131 version_group_id,
132 consensus_branch_id: branch_id,
133 fallback_lock_time: None,
134 expiry_height,
135 coin_type,
136 orchard_flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
137 ironwood_flags: crate::orchard::IRONWOOD_SPENDS_OUTPUTS_AND_CROSS_ADDRESS_ENABLED,
138 sapling_anchor,
139 orchard_anchor,
140 ironwood_anchor: None,
141 })
142 }
143
144 #[cfg(feature = "orchard")]
148 fn bundle_version(&self, pool: orchard::ValuePool) -> Option<orchard::bundle::BundleVersion> {
149 self.consensus_branch_id
150 .orchard_protocol_revision()
151 .and_then(|revision| bundle_version_for_revision(revision, pool))
152 }
153
154 pub fn with_fallback_lock_time(mut self, fallback: u32) -> Self {
155 self.fallback_lock_time = Some(fallback);
156 self
157 }
158
159 #[cfg(feature = "orchard")]
172 pub fn with_orchard_flags(
173 mut self,
174 orchard_flags: orchard::bundle::Flags,
175 ) -> Result<Self, Error> {
176 self.orchard_flags = orchard_flags
177 .to_byte(
178 self.bundle_version(orchard::ValuePool::Orchard)
179 .expect("`Creator::new` rejects branches that predate NU5"),
180 )
181 .ok_or(Error::UnrepresentableOrchardFlags)?;
182 Ok(self)
183 }
184
185 pub fn with_ironwood_anchor(mut self, ironwood_anchor: [u8; 32]) -> Result<Self, Error> {
193 let supports_ironwood = match self.tx_version {
194 V6_TX_VERSION => true,
195 #[cfg(zcash_unstable = "nutachyon")]
196 V7_TX_VERSION => true,
197 _ => false,
198 };
199 if !supports_ironwood {
200 return Err(Error::IronwoodNotSupported);
201 }
202 self.ironwood_anchor = Some(ironwood_anchor);
203 Ok(self)
204 }
205
206 #[cfg(feature = "orchard")]
215 pub fn with_ironwood_flags(
216 mut self,
217 ironwood_flags: orchard::bundle::Flags,
218 ) -> Result<Self, Error> {
219 self.ironwood_flags = ironwood_flags
220 .to_byte(
221 self.bundle_version(orchard::ValuePool::Ironwood)
222 .ok_or(Error::IronwoodNotSupported)?,
223 )
224 .ok_or(Error::UnrepresentableOrchardFlags)?;
225 Ok(self)
226 }
227
228 pub fn build(self) -> Result<Pczt, Error> {
236 let sapling = crate::sapling::Bundle {
237 spends: vec![],
238 outputs: vec![],
239 value_sum: 0,
240 anchor: self.sapling_anchor,
241 bsk: None,
242 };
243 let orchard = OrchardBundle {
244 actions: vec![],
245 flags: self.orchard_flags,
246 value_sum: (0, false),
247 anchor: self.orchard_anchor,
248 #[cfg(feature = "orchard")]
250 note_version: self
251 .bundle_version(orchard::ValuePool::Orchard)
252 .expect("`Creator::new` rejects branches that predate NU5")
253 .note_version(),
254 #[cfg(not(feature = "orchard"))]
255 note_version: crate::orchard::NoteVersion::V2,
256 zkproof: None,
257 bsk: None,
258 };
259
260 if self.tx_version == V5_TX_VERSION
261 && ((sapling.anchor.is_none() && !sapling.spends.is_empty())
262 || (orchard.anchor.is_none() && !orchard.actions.is_empty()))
263 {
264 return Err(Error::AnchorRequiredForV5);
265 }
266
267 Ok(Pczt {
268 global: crate::common::Global {
269 tx_version: self.tx_version,
270 version_group_id: self.version_group_id,
271 consensus_branch_id: self.consensus_branch_id.into(),
272 fallback_lock_time: self.fallback_lock_time,
273 expiry_height: self.expiry_height,
274 coin_type: self.coin_type,
275 tx_modifiable: INITIAL_TX_MODIFIABLE,
276 proprietary: BTreeMap::new(),
277 },
278 transparent: crate::transparent::Bundle {
279 inputs: vec![],
280 outputs: vec![],
281 },
282 sapling,
283 orchard,
284 ironwood: OrchardBundle {
285 flags: self.ironwood_flags,
286 anchor: self.ironwood_anchor,
287 ..crate::orchard::EMPTY_IRONWOOD
288 },
289 })
290 }
291
292 #[cfg(feature = "zcp-builder")]
298 pub fn build_from_parts<P: zcash_protocol::consensus::Parameters>(
299 parts: zcash_primitives::transaction::builder::PcztParts<P>,
300 ) -> Option<Pczt> {
301 let tx_version = match parts.version {
302 zcash_primitives::transaction::TxVersion::Sprout(_)
303 | zcash_primitives::transaction::TxVersion::V3 => None,
304 zcash_primitives::transaction::TxVersion::V4 => Some(V4_TX_VERSION),
305 zcash_primitives::transaction::TxVersion::V5 => Some(V5_TX_VERSION),
306 zcash_primitives::transaction::TxVersion::V6 => Some(V6_TX_VERSION),
307 #[cfg(zcash_unstable = "nutachyon")]
308 zcash_primitives::transaction::TxVersion::V7 => Some(V7_TX_VERSION),
309 }?;
310
311 let mut tx_modifiable = 0b0000_0000;
313 if parts.transparent.as_ref().is_some_and(|bundle| {
315 bundle.inputs().iter().any(|input| {
316 (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE
317 })
318 }) {
319 tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE;
320 }
321
322 Some(Pczt {
323 global: crate::common::Global {
324 tx_version,
325 version_group_id: parts.version.version_group_id(),
326 consensus_branch_id: parts.consensus_branch_id.into(),
327 fallback_lock_time: Some(parts.lock_time),
328 expiry_height: parts.expiry_height.into(),
329 coin_type: parts.params.network_type().coin_type(),
330 tx_modifiable,
331 proprietary: BTreeMap::new(),
332 },
333 transparent: parts
334 .transparent
335 .map(crate::transparent::Bundle::serialize_from)
336 .unwrap_or(crate::transparent::EMPTY_BUNDLE),
337 sapling: parts
338 .sapling
339 .map(crate::sapling::Bundle::serialize_from)
340 .unwrap_or(crate::sapling::EMPTY_BUNDLE),
341 orchard: parts
342 .orchard
343 .map(OrchardBundle::serialize_from)
344 .unwrap_or(crate::orchard::EMPTY_ORCHARD),
345 ironwood: parts
346 .ironwood
347 .map(OrchardBundle::serialize_from)
348 .unwrap_or(crate::orchard::EMPTY_IRONWOOD),
349 })
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 #[cfg(zcash_unstable = "nutachyon")]
356 use zcash_protocol::constants::{V7_TX_VERSION, V7_VERSION_GROUP_ID};
357 use zcash_protocol::{
358 consensus::BranchId,
359 constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID, V6_TX_VERSION, V6_VERSION_GROUP_ID},
360 };
361
362 use super::{Creator, Error};
363
364 #[test]
365 fn tx_version_follows_branch() {
366 let pczt = Creator::new(
367 BranchId::Nu6_2.into(),
368 10_000_000,
369 133,
370 Some([0; 32]),
371 Some([0; 32]),
372 )
373 .unwrap()
374 .build()
375 .unwrap();
376 assert_eq!(pczt.global.tx_version, V5_TX_VERSION);
377 assert_eq!(pczt.global.version_group_id, V5_VERSION_GROUP_ID);
378
379 let pczt = Creator::new(
380 BranchId::Nu6_3.into(),
381 10_000_000,
382 133,
383 Some([0; 32]),
384 Some([0; 32]),
385 )
386 .unwrap()
387 .build()
388 .unwrap();
389 assert_eq!(pczt.global.tx_version, V6_TX_VERSION);
390 assert_eq!(pczt.global.version_group_id, V6_VERSION_GROUP_ID);
391
392 #[cfg(zcash_unstable = "nutachyon")]
393 {
394 let pczt = Creator::new(
395 BranchId::NuTachyon.into(),
396 10_000_000,
397 133,
398 Some([0; 32]),
399 Some([0; 32]),
400 )
401 .unwrap()
402 .build()
403 .unwrap();
404 assert_eq!(pczt.global.tx_version, V7_TX_VERSION);
405 assert_eq!(pczt.global.version_group_id, V7_VERSION_GROUP_ID);
406 }
407 }
408
409 #[test]
410 fn optional_anchors_are_supported_for_empty_bundles() {
411 let pczt = Creator::new(BranchId::Nu6_2.into(), 10_000_000, 133, None, Some([0; 32]))
412 .unwrap()
413 .build()
414 .unwrap();
415 assert!(pczt.sapling.anchor.is_none());
416 assert_eq!(pczt.orchard.anchor, Some([0; 32]));
417
418 let pczt = Creator::new(BranchId::Nu6_2.into(), 10_000_000, 133, Some([0; 32]), None)
419 .unwrap()
420 .build()
421 .unwrap();
422 assert_eq!(pczt.sapling.anchor, Some([0; 32]));
423 assert!(pczt.orchard.anchor.is_none());
424
425 let pczt = Creator::new(BranchId::Nu6_3.into(), 10_000_000, 133, None, None)
426 .unwrap()
427 .build()
428 .unwrap();
429
430 assert!(pczt.sapling.anchor.is_none());
431 assert!(pczt.orchard.anchor.is_none());
432 }
433
434 #[test]
435 fn ironwood_anchor_requires_v6() {
436 assert!(matches!(
437 Creator::new(
438 BranchId::Nu6_2.into(),
439 10_000_000,
440 133,
441 Some([0; 32]),
442 Some([0; 32])
443 )
444 .unwrap()
445 .with_ironwood_anchor([1; 32]),
446 Err(Error::IronwoodNotSupported)
447 ));
448
449 let pczt = Creator::new(
450 BranchId::Nu6_3.into(),
451 10_000_000,
452 133,
453 Some([0; 32]),
454 Some([0; 32]),
455 )
456 .unwrap()
457 .with_ironwood_anchor([1; 32])
458 .unwrap()
459 .build()
460 .unwrap();
461 assert_eq!(pczt.ironwood.anchor, Some([1; 32]));
462 }
463}