Skip to main content

pczt/
roles.rs

1//! Implementations of the PCZT roles.
2//!
3//! The roles currently without an implementation are:
4//! - Constructor (anyone can contribute)
5//!   - Adds spends and outputs to the PCZT.
6//!   - Before any input or output may be added, the constructor must check the
7//!     `Global.tx_modifiable` field. Inputs may only be added if the Inputs Modifiable
8//!     flag is True. Outputs may only be added if the Outputs Modifiable flag is True.
9//!   - A single entity is likely to be both a Creator and Constructor.
10
11pub 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        // Extraction fails because we haven't run the IO Finalizer.
68        // Extraction fails in Sapling because we happen to extract it before Orchard.
69        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        // IO finalization fails on spends because we happen to check them first.
94        assert!(matches!(
95            IoFinalizer::new(pczt)
96                .finalize_io(UnwrapErr(SysRng))
97                .unwrap_err(),
98            io_finalizer::Error::NoSpends,
99        ));
100    }
101
102    /// Tests that a hostile Creator, Updater, or Combiner cannot induce the Signer or the
103    /// Spend Finalizer into producing a spend authorization over data that the coin being
104    /// spent does not commit to.
105    ///
106    /// These use a transparent-only PCZT so that the IO Finalizer does no shielded work
107    /// and no proving keys are needed.
108    #[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        /// The coin the victim is being asked to spend.
151        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        /// A PCZT spending a single P2PKH coin back to the victim's own address: the
184        /// transaction the victim believes it is authorizing.
185        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        /// A PCZT whose single input has been tampered with by a hostile counterparty.
201        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        /// A Signer over a PCZT tampered with by `f`, under `sighash_policy`.
208        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        /// The sighash for the single input of `pczt`, computed under a policy wide
218        /// enough to admit whatever sighash type it carries.
219        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        /// Signs `sighash` externally — as the counterparty who chose the input's
228        /// sighash type would — and offers the signature to a Signer with the default
229        /// policy.
230        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            // The coin being spent is P2PKH, so it commits to no redeem script at all;
241            // `Input::sign` would otherwise search this one for the signer’s pubkey and
242            // commit to it in the sighash.
243            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        /// The external-signature path has to authenticate the input for the same reason.
257        #[test]
258        fn signer_rejects_unauthenticated_redeem_script_when_appending() {
259            // The input is refused before the signature is examined, so what was signed
260            // does not matter — and the Signer will not compute a sighash over this input
261            // for us to sign in the first place.
262            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        /// The same holds for the sighash the Signer hands to an external signer: whoever
274        /// signs it is authorizing whatever it commits to.
275        #[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        /// Signs the single input of a PCZT tampered with by `f`, under `sighash_policy`.
345        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        /// A signed PCZT whose input uses `SIGHASH_NONE`.
355        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        /// The attacker's own coin, which they contribute to their own transaction.
389        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        /// A PCZT for a *different* transaction, which the victim never sees: it spends
401        /// the same coin of theirs alongside one of the attacker's, and pays the whole
402        /// balance to the attacker.
403        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        /// The victim's signature over the single input of the transaction they were
425        /// shown, with the trailing sighash byte stripped.
426        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        /// Whether `sig` authorizes the victim's input of `pczt`.
442        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        /// Why `SighashPolicy::ALL_ONLY` is the default: a signature the victim was
454        /// induced to make under `SIGHASH_NONE | SIGHASH_ANYONECANPAY` commits to
455        /// neither the outputs nor the other inputs, so it is equally a valid
456        /// authorization of a transaction the victim never saw, which pays the attacker.
457        ///
458        /// The Signer refuses to produce this signature at all unless the caller opts in,
459        /// which is what `signer_rejects_hostile_sighash_type` covers; this test is what
460        /// makes that refusal worth having.
461        #[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        /// The contrast: a `SIGHASH_ALL` signature over the same input commits to the
478        /// whole transaction the victim was shown, so it authorizes nothing else.
479        #[test]
480        fn an_all_signature_authorizes_nothing_else() {
481            let sig = victim_signature(SIGHASH_ALL, SighashPolicy::ALL_ONLY);
482
483            // Establishes that the negative below is a real refusal rather than, say, a
484            // signature that fails to verify against anything at all.
485            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        /// Inputs of a single transaction may deliberately use different sighash types:
511        /// contributors sign `SIGHASH_ALL | SIGHASH_ANYONECANPAY` so that the input set
512        /// stays open, and whoever assembles the transaction fixes it with a
513        /// `SIGHASH_ALL` signature over their own input.
514        #[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}