1pub mod creator;
12
13#[cfg(feature = "io-finalizer")]
14pub mod io_finalizer;
15
16pub mod verifier;
17
18pub mod updater;
19
20pub mod redactor;
21
22#[cfg(feature = "prover")]
23pub mod prover;
24
25#[cfg(feature = "signer")]
26pub mod signer;
27
28pub mod low_level_signer;
29
30pub mod combiner;
31
32#[cfg(feature = "spend-finalizer")]
33pub mod spend_finalizer;
34
35#[cfg(feature = "tx-extractor")]
36pub mod tx_extractor;
37
38#[cfg(test)]
39mod tests {
40 #[cfg(any(feature = "io-finalizer", feature = "tx-extractor"))]
41 use {
42 crate::roles::creator::Creator,
43 rand::{rand_core::UnwrapErr, rngs::SysRng},
44 zcash_protocol::consensus::BranchId,
45 };
46
47 #[cfg(feature = "io-finalizer")]
48 use crate::roles::io_finalizer::{self, IoFinalizer};
49
50 #[cfg(feature = "tx-extractor")]
51 use crate::roles::tx_extractor::{self, TransactionExtractor};
52
53 #[cfg(feature = "tx-extractor")]
54 #[test]
55 fn extract_fails_on_empty() {
56 let pczt = Creator::new(
57 BranchId::Nu6.into(),
58 10_000_000,
59 133,
60 Some([0; 32]),
61 Some([0; 32]),
62 )
63 .unwrap()
64 .build()
65 .unwrap();
66
67 assert!(matches!(
70 TransactionExtractor::new(pczt)
71 .extract(UnwrapErr(SysRng))
72 .unwrap_err(),
73 tx_extractor::Error::Sapling(tx_extractor::SaplingError::Extract(
74 sapling::pczt::TxExtractorError::MissingBindingSignatureSigningKey
75 )),
76 ));
77 }
78
79 #[cfg(feature = "io-finalizer")]
80 #[test]
81 fn io_finalizer_fails_on_empty() {
82 let pczt = Creator::new(
83 BranchId::Nu6.into(),
84 10_000_000,
85 133,
86 Some([0; 32]),
87 Some([0; 32]),
88 )
89 .unwrap()
90 .build()
91 .unwrap();
92
93 assert!(matches!(
95 IoFinalizer::new(pczt)
96 .finalize_io(UnwrapErr(SysRng))
97 .unwrap_err(),
98 io_finalizer::Error::NoSpends,
99 ));
100 }
101
102 #[cfg(all(
109 feature = "io-finalizer",
110 feature = "signer",
111 feature = "spend-finalizer",
112 feature = "zcp-builder"
113 ))]
114 mod transparent_authentication {
115 use alloc::vec::Vec;
116
117 use ::transparent::{
118 address::TransparentAddress,
119 bundle::{OutPoint, TxOut},
120 pczt::{SignerError, SpendFinalizerError, VerifyError},
121 sighash::{
122 SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE, SighashPolicy,
123 },
124 };
125 use rand::{rand_core::UnwrapErr, rngs::SysRng};
126 use zcash_primitives::transaction::{
127 builder::{BuildConfig, Builder, BundlePadding, PcztResult},
128 fees::zip317,
129 };
130 use zcash_protocol::{consensus::MainNetwork, value::Zatoshis};
131
132 use crate::{
133 Pczt,
134 roles::{
135 creator::Creator, io_finalizer::IoFinalizer, signer, signer::Signer,
136 spend_finalizer, spend_finalizer::SpendFinalizer,
137 },
138 };
139
140 const SIGHASH_ALL_ANYONECANPAY: u8 = SIGHASH_ALL | SIGHASH_ANYONECANPAY;
141
142 fn secret_key() -> secp256k1::SecretKey {
143 secp256k1::SecretKey::from_secret_bytes([1; 32]).expect("valid")
144 }
145
146 fn public_key(sk: &secp256k1::SecretKey) -> secp256k1::PublicKey {
147 sk.public_key()
148 }
149
150 fn victim_coin() -> (OutPoint, TxOut) {
152 let addr = TransparentAddress::from_pubkey(&public_key(&secret_key()));
153 (
154 OutPoint::new([1; 32], 1),
155 TxOut::new(Zatoshis::const_from_u64(1_000_000), addr.script().into()),
156 )
157 }
158
159 fn builder() -> Builder<MainNetwork, ()> {
160 Builder::new(
161 MainNetwork,
162 10_000_000.into(),
163 BuildConfig::Standard {
164 sapling_anchor: None,
165 orchard_anchor: None,
166 ironwood_anchor: None,
167 orchard_padding: BundlePadding::DEFAULT,
168 ironwood_padding: BundlePadding::DEFAULT,
169 },
170 )
171 }
172
173 fn finalize(builder: Builder<MainNetwork, ()>) -> Pczt {
174 let PcztResult { pczt_parts, .. } = builder
175 .build_for_pczt(UnwrapErr(SysRng), &zip317::FeeRule::standard())
176 .unwrap();
177
178 IoFinalizer::new(Creator::build_from_parts(pczt_parts).unwrap())
179 .finalize_io(UnwrapErr(SysRng))
180 .unwrap()
181 }
182
183 fn transparent_pczt() -> Pczt {
186 let pubkey = public_key(&secret_key());
187 let addr = TransparentAddress::from_pubkey(&pubkey);
188 let (utxo, coin) = victim_coin();
189
190 let mut builder = builder();
191 builder
192 .add_transparent_p2pkh_input(pubkey, utxo, coin)
193 .unwrap();
194 builder
195 .add_transparent_output(&addr, Zatoshis::const_from_u64(990_000))
196 .unwrap();
197 finalize(builder)
198 }
199
200 fn tampered_pczt(f: impl FnOnce(&mut crate::transparent::Input)) -> Pczt {
202 let mut pczt = transparent_pczt();
203 f(&mut pczt.transparent.inputs[0]);
204 pczt
205 }
206
207 fn signer(
209 f: impl FnOnce(&mut crate::transparent::Input),
210 sighash_policy: SighashPolicy,
211 ) -> Signer {
212 Signer::new(tampered_pczt(f))
213 .unwrap()
214 .with_transparent_sighash_policy(sighash_policy)
215 }
216
217 fn unguarded_sighash(pczt: Pczt) -> [u8; 32] {
220 Signer::new(pczt)
221 .unwrap()
222 .with_transparent_sighash_policy(SighashPolicy::ANY)
223 .transparent_sighash(0)
224 .unwrap()
225 }
226
227 fn append(pczt: Pczt, sighash: [u8; 32]) -> Result<(), signer::Error> {
231 let sig =
232 secp256k1::ecdsa::sign(secp256k1::Message::from_digest(sighash), &secret_key());
233 Signer::new(pczt)
234 .unwrap()
235 .append_transparent_signature(0, sig)
236 }
237
238 #[test]
239 fn signer_rejects_unauthenticated_redeem_script() {
240 let mut signer = signer(
244 |input| input.redeem_script = Some(vec![0x51]),
245 SighashPolicy::ALL_ONLY,
246 );
247
248 assert!(matches!(
249 signer.sign_transparent(0, &secret_key()),
250 Err(signer::Error::TransparentSign(SignerError::InvalidInput(
251 VerifyError::NotP2sh
252 ))),
253 ));
254 }
255
256 #[test]
258 fn signer_rejects_unauthenticated_redeem_script_when_appending() {
259 assert!(matches!(
263 append(
264 tampered_pczt(|input| input.redeem_script = Some(vec![0x51])),
265 [0; 32],
266 ),
267 Err(signer::Error::TransparentSign(SignerError::InvalidInput(
268 VerifyError::NotP2sh
269 ))),
270 ));
271 }
272
273 #[test]
276 fn signer_rejects_unauthenticated_redeem_script_when_computing_sighash() {
277 let signer = signer(
278 |input| input.redeem_script = Some(vec![0x51]),
279 SighashPolicy::ALL_ONLY,
280 );
281
282 assert!(matches!(
283 signer.transparent_sighash(0),
284 Err(signer::Error::TransparentSign(SignerError::InvalidInput(
285 VerifyError::NotP2sh
286 ))),
287 ));
288 }
289
290 #[test]
291 fn signer_rejects_hostile_sighash_type() {
292 let mut signer = signer(
293 |input| input.sighash_type = SIGHASH_NONE | SIGHASH_ANYONECANPAY,
294 SighashPolicy::ALL_ONLY,
295 );
296
297 assert!(matches!(
298 signer.sign_transparent(0, &secret_key()),
299 Err(signer::Error::TransparentSign(
300 SignerError::DisallowedSighashType(_)
301 )),
302 ));
303 }
304
305 #[test]
306 fn signer_rejects_hostile_sighash_type_when_appending() {
307 let pczt =
308 tampered_pczt(|input| input.sighash_type = SIGHASH_NONE | SIGHASH_ANYONECANPAY);
309 let sighash = unguarded_sighash(pczt.clone());
310
311 assert!(matches!(
312 append(pczt, sighash),
313 Err(signer::Error::TransparentSign(
314 SignerError::DisallowedSighashType(_)
315 )),
316 ));
317 }
318
319 #[test]
320 fn signer_rejects_hostile_sighash_type_when_computing_sighash() {
321 let signer = signer(
322 |input| input.sighash_type = SIGHASH_NONE | SIGHASH_ANYONECANPAY,
323 SighashPolicy::ALL_ONLY,
324 );
325
326 assert!(matches!(
327 signer.transparent_sighash(0),
328 Err(signer::Error::TransparentSign(
329 SignerError::DisallowedSighashType(_)
330 )),
331 ));
332 }
333
334 #[test]
335 fn signer_accepts_hostile_sighash_type_under_explicit_policy() {
336 let mut signer = signer(
337 |input| input.sighash_type = SIGHASH_NONE | SIGHASH_ANYONECANPAY,
338 SighashPolicy::ANY,
339 );
340
341 assert!(signer.sign_transparent(0, &secret_key()).is_ok());
342 }
343
344 fn signed(
346 f: impl FnOnce(&mut crate::transparent::Input),
347 sighash_policy: SighashPolicy,
348 ) -> Pczt {
349 let mut signer = signer(f, sighash_policy);
350 signer.sign_transparent(0, &secret_key()).unwrap();
351 signer.finish()
352 }
353
354 fn signed_sighash_none() -> Pczt {
356 signed(
357 |input| input.sighash_type = SIGHASH_NONE,
358 SighashPolicy::ANY,
359 )
360 }
361
362 #[test]
363 fn finalizer_rejects_non_all_sighash_type() {
364 assert!(matches!(
365 SpendFinalizer::new(signed_sighash_none()).finalize_spends(),
366 Err(spend_finalizer::Error::TransparentFinalize(
367 SpendFinalizerError::DisallowedSighashType(_)
368 )),
369 ));
370 }
371
372 #[test]
373 fn finalizer_accepts_non_all_sighash_type_under_explicit_policy() {
374 assert!(
375 SpendFinalizer::new(signed_sighash_none())
376 .with_sighash_policy(SighashPolicy::ANY)
377 .finalize_spends()
378 .is_ok()
379 );
380 }
381
382 #[test]
383 fn finalizer_accepts_all_sighash_type() {
384 let pczt = signed(|_| (), SighashPolicy::ALL_ONLY);
385 assert!(SpendFinalizer::new(pczt).finalize_spends().is_ok());
386 }
387
388 fn attacker_coin() -> (secp256k1::PublicKey, OutPoint, TxOut) {
390 let pubkey =
391 public_key(&secp256k1::SecretKey::from_secret_bytes([2; 32]).expect("valid"));
392 let addr = TransparentAddress::from_pubkey(&pubkey);
393 (
394 pubkey,
395 OutPoint::new([2; 32], 0),
396 TxOut::new(Zatoshis::const_from_u64(1_000_000), addr.script().into()),
397 )
398 }
399
400 fn attacker_pczt(sighash_type: u8) -> Pczt {
404 let (attacker_pubkey, attacker_utxo, attacker_coin) = attacker_coin();
405 let attacker_addr = TransparentAddress::from_pubkey(&attacker_pubkey);
406 let (victim_utxo, victim_coin) = victim_coin();
407
408 let mut builder = builder();
409 builder
410 .add_transparent_p2pkh_input(public_key(&secret_key()), victim_utxo, victim_coin)
411 .unwrap();
412 builder
413 .add_transparent_p2pkh_input(attacker_pubkey, attacker_utxo, attacker_coin)
414 .unwrap();
415 builder
416 .add_transparent_output(&attacker_addr, Zatoshis::const_from_u64(1_990_000))
417 .unwrap();
418
419 let mut pczt = finalize(builder);
420 pczt.transparent.inputs[0].sighash_type = sighash_type;
421 pczt
422 }
423
424 fn victim_signature(sighash_type: u8, sighash_policy: SighashPolicy) -> Vec<u8> {
427 let pczt = signed(|input| input.sighash_type = sighash_type, sighash_policy);
428 let mut sig = pczt.transparent.inputs[0]
429 .partial_signatures
430 .get(&public_key(&secret_key()).serialize())
431 .expect("the victim signed")
432 .clone();
433 assert_eq!(
434 sig.pop(),
435 Some(sighash_type),
436 "signature carries its sighash type"
437 );
438 sig
439 }
440
441 fn authorizes(sig: &[u8], pczt: Pczt) -> bool {
443 let sighash = unguarded_sighash(pczt);
444
445 secp256k1::ecdsa::verify(
446 &secp256k1::ecdsa::Signature::from_der(sig).expect("DER-encoded"),
447 secp256k1::Message::from_digest(sighash),
448 &public_key(&secret_key()),
449 )
450 .is_ok()
451 }
452
453 #[test]
462 fn a_non_all_signature_authorizes_the_attackers_transaction() {
463 let hostile = SIGHASH_NONE | SIGHASH_ANYONECANPAY;
464 let sig = victim_signature(hostile, SighashPolicy::ANY);
465
466 assert!(
467 authorizes(&sig, tampered_pczt(|input| input.sighash_type = hostile)),
468 "the victim's signature should authorize the transaction they were shown",
469 );
470 assert!(
471 authorizes(&sig, attacker_pczt(hostile)),
472 "and, because it commits to neither the outputs nor the other inputs, \
473 the attacker's transaction as well",
474 );
475 }
476
477 #[test]
480 fn an_all_signature_authorizes_nothing_else() {
481 let sig = victim_signature(SIGHASH_ALL, SighashPolicy::ALL_ONLY);
482
483 assert!(
486 authorizes(&sig, transparent_pczt()),
487 "the victim's signature should authorize the transaction they were shown",
488 );
489 assert!(
490 !authorizes(&sig, attacker_pczt(SIGHASH_ALL)),
491 "but not the attacker's",
492 );
493 }
494
495 #[test]
496 fn finalizer_accepts_sighash_single() {
497 let pczt = signed(
498 |input| input.sighash_type = SIGHASH_SINGLE,
499 SighashPolicy::ANY,
500 );
501
502 assert!(
503 SpendFinalizer::new(pczt)
504 .with_sighash_policy(SighashPolicy::ANY)
505 .finalize_spends()
506 .is_ok()
507 );
508 }
509
510 #[test]
515 fn finalizer_accepts_mixed_sighash_types() {
516 let contributor_sk = secp256k1::SecretKey::from_secret_bytes([3; 32]).expect("valid");
517 let contributor_pubkey = public_key(&contributor_sk);
518 let contributor_utxo = OutPoint::new([3; 32], 0);
519 let contributor_coin = TxOut::new(
520 Zatoshis::const_from_u64(1_000_000),
521 TransparentAddress::from_pubkey(&contributor_pubkey)
522 .script()
523 .into(),
524 );
525 let (assembler_utxo, assembler_coin) = victim_coin();
526 let addr = TransparentAddress::from_pubkey(&public_key(&secret_key()));
527
528 let mut builder = builder();
529 builder
530 .add_transparent_p2pkh_input(
531 public_key(&secret_key()),
532 assembler_utxo,
533 assembler_coin,
534 )
535 .unwrap();
536 builder
537 .add_transparent_p2pkh_input(contributor_pubkey, contributor_utxo, contributor_coin)
538 .unwrap();
539 builder
540 .add_transparent_output(&addr, Zatoshis::const_from_u64(1_990_000))
541 .unwrap();
542
543 let mut pczt = finalize(builder);
544 pczt.transparent.inputs[1].sighash_type = SIGHASH_ALL_ANYONECANPAY;
545
546 let mut signer = Signer::new(pczt)
547 .unwrap()
548 .with_transparent_sighash_policy(SighashPolicy::ANY);
549 signer.sign_transparent(0, &secret_key()).unwrap();
550 signer.sign_transparent(1, &contributor_sk).unwrap();
551
552 assert!(
553 SpendFinalizer::new(signer.finish())
554 .with_sighash_policy(SighashPolicy::ANY)
555 .finalize_spends()
556 .is_ok()
557 );
558 }
559 }
560}