Skip to main content

zebra_chain/transaction/arbitrary/
shielded.rs

1//! Builders for shielded bundles on the `zcash_primitives` transaction types.
2//!
3//! Zebra's [`Transaction`](crate::transaction::Transaction) wraps
4//! `zcash_primitives::transaction::Transaction`, whose bundles are owned by the upstream crates
5//! and cannot be mutated in place. Tests therefore cannot reach in and edit shielded data the way
6//! they could when `Transaction` was an enum of Zebra-owned structs.
7//!
8//! This module builds Orchard-shaped bundles (used by both the Orchard and Ironwood pools)
9//! directly from their constituent parts, so property tests and vector tests can exercise
10//! shielded code paths.
11//!
12//! The bundles are structurally valid — they carry real, canonically-encoded Pallas points and a
13//! proof of the canonical length, so they serialize, deserialize, and commit correctly — but they
14//! are **not** consensus-valid: the proofs are zeroed and the signatures are arbitrary. They are
15//! for exercising parsing, indexing, and nullifier bookkeeping, not proof verification.
16
17use group::{
18    ff::{FromUniformBytes, PrimeField},
19    prime::PrimeCurveAffine,
20    GroupEncoding,
21};
22use halo2::pasta::pallas;
23use nonempty::NonEmpty;
24
25use orchard::{
26    bundle::{Authorized, BundleVersion, Flags},
27    note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext},
28    primitives::redpallas::{self, SpendAuth},
29    value::ValueCommitment,
30    Action, Anchor, Bundle, Proof,
31};
32use zcash_protocol::value::ZatBalance;
33
34/// Derives a canonically-encoded `pallas::Base` from a `seed`.
35///
36/// Distinct seeds give distinct field elements, so callers can build actions with distinct
37/// nullifiers and note commitments.
38fn base_from_seed(seed: u64) -> pallas::Base {
39    let mut bytes = [0u8; 64];
40    bytes[..8].copy_from_slice(&seed.to_le_bytes());
41    // Domain-separate from `scalar_from_seed` so the two never coincide.
42    bytes[63] = 0x01;
43    pallas::Base::from_uniform_bytes(&bytes)
44}
45
46/// Derives a canonically-encoded `pallas::Scalar` from a `seed`.
47fn scalar_from_seed(seed: u64) -> pallas::Scalar {
48    let mut bytes = [0u8; 64];
49    bytes[..8].copy_from_slice(&seed.to_le_bytes());
50    bytes[63] = 0x02;
51    pallas::Scalar::from_uniform_bytes(&bytes)
52}
53
54/// Builds a non-identity RedPallas verification key from a `seed`.
55///
56/// [`Action::from_parts`] rejects an identity `rk`, so this derives the key from a signing key
57/// rather than picking bytes directly.
58fn verification_key_from_seed(seed: u64) -> redpallas::VerificationKey<SpendAuth> {
59    let sk_bytes = scalar_from_seed(seed).to_repr();
60    let sk = reddsa::SigningKey::<reddsa::orchard::SpendAuth>::try_from(sk_bytes)
61        .expect("a canonical scalar is a valid signing key");
62    let pk_bytes: [u8; 32] = reddsa::VerificationKey::from(&sk).into();
63
64    redpallas::VerificationKey::try_from(pk_bytes)
65        .expect("a key derived from a signing key is a valid, non-identity verification key")
66}
67
68/// Builds a structurally valid Orchard [`Action`] from a `seed`.
69///
70/// Each `seed` yields a distinct nullifier and note commitment, so a bundle's actions do not
71/// collide, and bundles built from different seed ranges have disjoint nullifier sets.
72fn fake_action(seed: u64) -> Action<redpallas::Signature<SpendAuth>> {
73    // `cv_net` is unconstrained by `Action::from_parts`, so the identity point is fine here;
74    // it just has to be a canonical point encoding.
75    let cv_net = ValueCommitment::from_bytes(&pallas::Affine::identity().to_bytes())
76        .expect("the identity point is a canonical value commitment encoding");
77
78    let nullifier = Nullifier::from_bytes(&base_from_seed(seed).to_repr())
79        .expect("a canonical base field element is a valid nullifier");
80
81    let cmx = ExtractedNoteCommitment::from_bytes(&base_from_seed(seed ^ 0xFFFF).to_repr())
82        .expect("a canonical base field element is a valid note commitment");
83
84    // `Action::from_parts` rejects an identity ephemeral key, so use the curve generator.
85    let encrypted_note = TransmittedNoteCiphertext {
86        epk_bytes: pallas::Affine::generator().to_bytes(),
87        enc_ciphertext: [0u8; 580],
88        out_ciphertext: [0u8; 80],
89    };
90
91    let spend_auth_sig = redpallas::Signature::<SpendAuth>::from([0u8; 64]);
92
93    Action::from_parts(
94        nullifier,
95        verification_key_from_seed(seed),
96        cmx,
97        encrypted_note,
98        cv_net,
99        spend_auth_sig,
100    )
101    .expect("action parts are valid: rk and epk are non-identity")
102}
103
104/// Builds a structurally valid Orchard-shaped bundle.
105///
106/// * `flags` must be representable under `bundle_version`, otherwise this panics. In particular
107///   the `enableCrossAddress` bit is representable only for [`BundleVersion::ironwood_v3`].
108/// * `n_actions` must be non-zero.
109/// * `seed` selects the action contents; bundles built with different seeds have disjoint
110///   nullifier sets, which matters for the mempool and state conflict tests.
111///
112/// The proof is zeroed but has the canonical length for `n_actions`, so the bundle passes
113/// [`Bundle::try_from_parts`]'s proof-size check and Zebra's own canonical-size rule.
114pub fn fake_orchard_bundle(
115    flags: Flags,
116    value_balance: ZatBalance,
117    n_actions: usize,
118    seed: u64,
119    bundle_version: BundleVersion,
120) -> Bundle<Authorized, ZatBalance> {
121    assert!(n_actions > 0, "an Orchard bundle must have some actions");
122
123    let actions: Vec<_> = (0..n_actions)
124        .map(|i| fake_action(seed.wrapping_add(i as u64)))
125        .collect();
126
127    let authorization = Authorized::from_parts(
128        Proof::new(vec![0u8; Proof::expected_proof_size(n_actions)]),
129        redpallas::Signature::<redpallas::Binding>::from([0u8; 64]),
130    );
131
132    Bundle::try_from_parts(
133        NonEmpty::from_vec(actions).expect("n_actions is non-zero"),
134        flags,
135        value_balance,
136        Anchor::from(base_from_seed(seed ^ 0xA11C)),
137        authorization,
138        bundle_version,
139    )
140    .expect("the proof has the canonical length and the flags are representable")
141}
142
143/// Builds an Orchard-shaped bundle whose actions all share a single nullifier.
144///
145/// Used by duplicate-nullifier consensus tests, which need a within-transaction double spend.
146pub fn fake_orchard_bundle_duplicate_nullifiers(
147    flags: Flags,
148    value_balance: ZatBalance,
149    n_actions: usize,
150    seed: u64,
151    bundle_version: BundleVersion,
152) -> Bundle<Authorized, ZatBalance> {
153    assert!(n_actions > 0, "an Orchard bundle must have some actions");
154
155    // Every action is built from the same seed, so they share a nullifier.
156    let actions: Vec<_> = (0..n_actions).map(|_| fake_action(seed)).collect();
157
158    let authorization = Authorized::from_parts(
159        Proof::new(vec![0u8; Proof::expected_proof_size(n_actions)]),
160        redpallas::Signature::<redpallas::Binding>::from([0u8; 64]),
161    );
162
163    Bundle::try_from_parts(
164        NonEmpty::from_vec(actions).expect("n_actions is non-zero"),
165        flags,
166        value_balance,
167        Anchor::from(base_from_seed(seed ^ 0xA11C)),
168        authorization,
169        bundle_version,
170    )
171    .expect("the proof has the canonical length and the flags are representable")
172}
173
174/// The seed used for fields the note-encryption vectors do not constrain.
175const NOTE_VECTOR_SEED: u64 = 0x4E4F_5445;
176
177/// Builds a single-action Orchard-shaped bundle carrying caller-supplied note-encryption fields.
178///
179/// The note-encryption test vectors fix `cv_net`, `rho`, `cmx`, `ephemeralKey`, `encCiphertext`
180/// and `outCiphertext`; everything else (the spend authorization key and signature, the anchor,
181/// the proof) is unconstrained by those vectors and is filled in with the same dummy values as
182/// [`fake_orchard_bundle`].
183///
184/// `flags` must have outputs enabled for the resulting transaction to count as having shielded
185/// outputs, and must be representable under `bundle_version`.
186#[allow(clippy::too_many_arguments)]
187pub fn fake_orchard_bundle_with_note(
188    flags: Flags,
189    value_balance: ZatBalance,
190    bundle_version: BundleVersion,
191    cv_net: &[u8; 32],
192    nullifier: &[u8; 32],
193    cmx: &[u8; 32],
194    epk_bytes: [u8; 32],
195    enc_ciphertext: [u8; 580],
196    out_ciphertext: [u8; 80],
197) -> Bundle<Authorized, ZatBalance> {
198    let action = Action::from_parts(
199        Nullifier::from_bytes(nullifier).expect("the test vector's rho is a valid nullifier"),
200        // `rk` is not covered by the note-encryption vectors, and decryption does not read it.
201        verification_key_from_seed(NOTE_VECTOR_SEED),
202        ExtractedNoteCommitment::from_bytes(cmx)
203            .expect("the test vector's cmx is a valid note commitment"),
204        TransmittedNoteCiphertext {
205            epk_bytes,
206            enc_ciphertext,
207            out_ciphertext,
208        },
209        ValueCommitment::from_bytes(cv_net).expect("the test vector's cv_net is a valid point"),
210        redpallas::Signature::<SpendAuth>::from([0u8; 64]),
211    )
212    .expect("the test vector supplies a non-identity rk and ephemeral key");
213
214    let authorization = Authorized::from_parts(
215        Proof::new(vec![0u8; Proof::expected_proof_size(1)]),
216        redpallas::Signature::<redpallas::Binding>::from([0u8; 64]),
217    );
218
219    Bundle::try_from_parts(
220        NonEmpty::from_vec(vec![action]).expect("exactly one action"),
221        flags,
222        value_balance,
223        Anchor::from(base_from_seed(0xA11C)),
224        authorization,
225        bundle_version,
226    )
227    .expect("the proof has the canonical length and the flags are representable")
228}
229
230/// Returns the flag set with outputs enabled that is representable under `bundle_version`.
231///
232/// The Orchard pool requires cross-address transfers to be enabled before NU6.3 and disabled
233/// from NU6.3 onward, so the representable flag set depends on the version.
234pub fn outputs_enabled_flags(bundle_version: BundleVersion) -> Flags {
235    if Flags::ENABLED.to_byte(bundle_version).is_some() {
236        Flags::ENABLED
237    } else {
238        Flags::CROSS_ADDRESS_DISABLED
239    }
240}
241
242/// Builds a bundle for `pool` that is valid under `branch_id`, or `None` if the pool is not
243/// defined for that consensus branch (Ironwood before NU6.3).
244///
245/// The bundle version — and therefore which flag sets are representable — is derived from
246/// `branch_id`, so a bundle built here always matches the transaction it will be placed in. The
247/// flags are the maximal set representable under that version: the Orchard pool disables
248/// cross-address transfers from NU6.3 onward and requires them enabled before, while the
249/// Ironwood pool encodes the choice in its flag byte.
250pub fn fake_bundle_for_branch(
251    branch_id: zcash_protocol::consensus::BranchId,
252    pool: ::orchard::ValuePool,
253    n_actions: usize,
254    seed: u64,
255) -> Option<Bundle<Authorized, ZatBalance>> {
256    let bundle_version =
257        zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
258            branch_id, pool,
259        )?;
260
261    // Pick the flag set that this bundle version can encode.
262    let flags = if Flags::ENABLED.to_byte(bundle_version).is_some() {
263        Flags::ENABLED
264    } else {
265        Flags::CROSS_ADDRESS_DISABLED
266    };
267
268    Some(fake_orchard_bundle(
269        flags,
270        ZatBalance::from_i64(0).expect("zero is a valid balance"),
271        n_actions,
272        seed,
273        bundle_version,
274    ))
275}
276
277/// The size of one serialized Orchard action: cv, nullifier, rk, cmx and ephemeralKey
278/// (32 bytes each), then encCiphertext and outCiphertext.
279pub const ACTION_WIRE_SIZE: usize = 32 * 5 + 580 + 80;
280
281/// The offset of the `flagsOrchard` byte of the Orchard bundle, within a v6 transaction that has
282/// no transparent and no Sapling bundle, and whose Orchard bundle has `n_actions` actions.
283///
284/// Layout: header, nVersionGroupId, nConsensusBranchId, lockTime, nExpiryHeight (4 bytes each),
285/// then the empty transparent bundle (two zero CompactSize counts), the empty Sapling bundle
286/// (two zero CompactSize counts), then `nActionsOrchard` and the actions themselves.
287///
288/// `n_actions` must be small enough to encode as a one-byte CompactSize.
289pub fn v6_orchard_flags_offset(n_actions: usize) -> usize {
290    assert!(n_actions < 253, "n_actions must be a one-byte CompactSize");
291    (4 * 5) + 2 + 2 + 1 + n_actions * ACTION_WIRE_SIZE
292}
293
294/// The offset of the `flagsOrchard` byte of the *Ironwood* bundle, within a v6 transaction that
295/// has no transparent and no Sapling bundle, an *empty* Orchard bundle, and an Ironwood bundle
296/// with `n_ironwood_actions` actions.
297///
298/// An empty Orchard bundle is a single zero `nActionsOrchard` byte.
299pub fn v6_ironwood_flags_offset(n_ironwood_actions: usize) -> usize {
300    assert!(
301        n_ironwood_actions < 253,
302        "n_ironwood_actions must be a one-byte CompactSize"
303    );
304    (4 * 5) + 2 + 2 + 1 + 1 + n_ironwood_actions * ACTION_WIRE_SIZE
305}
306
307/// The offset of the first Orchard action's `rk` field, within a v5 transaction that has no
308/// transparent and no Sapling bundle.
309///
310/// Layout: header, nVersionGroupId, nConsensusBranchId, lockTime, nExpiryHeight (4 bytes each),
311/// then the empty transparent bundle (two zero CompactSize counts), the empty Sapling bundle
312/// (two zero CompactSize counts), `nActionsOrchard`, then the action's `cv` and `nullifier`.
313pub const V5_FIRST_ACTION_RK_OFFSET: usize = (4 * 5) + 2 + 2 + 1 + 32 + 32;
314
315/// Builds a V6 transaction carrying the given Orchard and Ironwood bundles.
316///
317/// This is the constructor the v6 wire-format vector tests use: it takes bundles built by
318/// [`fake_orchard_bundle`] and puts them in the two v6 Orchard-shaped slots.
319pub fn fake_v6_transaction(
320    network_upgrade: crate::parameters::NetworkUpgrade,
321    orchard_bundle: Option<Bundle<Authorized, ZatBalance>>,
322    ironwood_bundle: Option<Bundle<Authorized, ZatBalance>>,
323) -> crate::transaction::Transaction {
324    crate::transaction::Transaction::test_v6_with_bundles(
325        network_upgrade,
326        Vec::new(),
327        Vec::new(),
328        crate::transaction::LockTime::min_lock_time_timestamp(),
329        crate::block::Height(0),
330        orchard_bundle,
331        ironwood_bundle,
332    )
333}
334
335/// Returns a copy of `tx` whose Orchard bundle carries `value_balance`.
336///
337/// The bundle is owned by `zcash_primitives` and cannot be mutated in place, so this rebuilds
338/// the transaction. Panics if `tx` has no Orchard bundle.
339pub fn with_orchard_value_balance(
340    tx: crate::transaction::Transaction,
341    value_balance: i64,
342) -> crate::transaction::Transaction {
343    let balance = ZatBalance::from_i64(value_balance).expect("a valid signed amount");
344
345    let bundle = tx
346        .orchard_bundle()
347        .expect("the transaction must have an Orchard bundle")
348        .clone()
349        .try_map_value_balance::<_, (), _>(|_| Ok(balance))
350        .expect("the mapping cannot fail");
351
352    tx.with_orchard_bundle(Some(bundle))
353}
354
355/// Returns a copy of `tx` carrying a dummy single-action Orchard bundle.
356///
357/// The bundle has no flags set and a zero value balance, for structural consensus-rule tests
358/// that only care that *an* Orchard bundle is present.
359pub fn insert_fake_orchard_shielded_data(
360    tx: crate::transaction::Transaction,
361) -> crate::transaction::Transaction {
362    let branch_id = tx.inner().consensus_branch_id();
363    let bundle = fake_bundle_for_branch(branch_id, ::orchard::ValuePool::Orchard, 1, 0xF00D)
364        .expect("the Orchard pool is defined for this transaction's branch");
365
366    tx.with_orchard_bundle(Some(bundle))
367}
368
369/// Returns a copy of `tx` whose Orchard bundle carries `flags`.
370///
371/// Panics if `tx` has no Orchard bundle, or if `flags` are not representable under the bundle's
372/// version.
373pub fn with_orchard_flags(
374    tx: crate::transaction::Transaction,
375    flags: Flags,
376) -> crate::transaction::Transaction {
377    let bundle = tx
378        .orchard_bundle()
379        .expect("the transaction must have an Orchard bundle");
380
381    let rebuilt = Bundle::try_from_parts(
382        bundle.actions().clone(),
383        flags,
384        *bundle.value_balance(),
385        *bundle.anchor(),
386        bundle.authorization().clone(),
387        bundle.bundle_version(),
388    )
389    .expect("the flags must be representable under the bundle version");
390
391    tx.with_orchard_bundle(Some(rebuilt))
392}
393
394/// Returns a copy of `tx` whose Orchard bundle keeps its effects but has garbage authorizing
395/// data: a corrupt proof, binding signature, and spend authorization signatures.
396///
397/// Per ZIP-244 the txid covers only a transaction's effects, so the returned transaction has the
398/// same txid as `tx` while being unverifiable. Used to check that a cached verification result
399/// for one cannot stand in for the other.
400///
401/// The bundle version must not enforce a canonical proof size (i.e. pre-NU6.2 Orchard), since the
402/// point is to install a proof that is not a real one.
403pub fn with_garbage_orchard_authorization(
404    tx: crate::transaction::Transaction,
405) -> crate::transaction::Transaction {
406    let bundle = tx
407        .orchard_bundle()
408        .expect("the transaction must have an Orchard bundle");
409
410    // Rebuild every action with a garbage spend authorization signature, preserving its effects.
411    let actions: Vec<_> = bundle
412        .actions()
413        .iter()
414        .map(|action| {
415            Action::from_parts(
416                *action.nullifier(),
417                action.rk().clone(),
418                *action.cmx(),
419                TransmittedNoteCiphertext {
420                    epk_bytes: action.encrypted_note().epk_bytes,
421                    enc_ciphertext: action.encrypted_note().enc_ciphertext,
422                    out_ciphertext: action.encrypted_note().out_ciphertext,
423                },
424                action.cv_net().clone(),
425                redpallas::Signature::<SpendAuth>::from([0xFF; 64]),
426            )
427            .expect("the effects are copied from a valid action")
428        })
429        .collect();
430
431    let authorization = Authorized::from_parts(
432        Proof::new(vec![0xDE, 0xAD, 0xBE, 0xEF]),
433        redpallas::Signature::<redpallas::Binding>::from([0xFF; 64]),
434    );
435
436    let rebuilt = Bundle::try_from_parts(
437        NonEmpty::from_vec(actions).expect("the source bundle is non-empty"),
438        *bundle.flags(),
439        *bundle.value_balance(),
440        *bundle.anchor(),
441        authorization,
442        bundle.bundle_version(),
443    )
444    .expect("the bundle version must not enforce a canonical proof size");
445
446    tx.with_orchard_bundle(Some(rebuilt))
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    use crate::{
454        parameters::NetworkUpgrade,
455        serialization::{ZcashDeserializeInto, ZcashSerialize},
456        transaction::Transaction,
457    };
458
459    /// The bundles this module builds must survive a wire-format round trip, otherwise the tests
460    /// that rely on them are testing a shape the parser would never accept.
461    #[test]
462    fn fake_bundles_round_trip() {
463        let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
464
465        let orchard = fake_orchard_bundle(
466            Flags::CROSS_ADDRESS_DISABLED,
467            zero,
468            2,
469            1,
470            BundleVersion::orchard_v3(),
471        );
472        let ironwood =
473            fake_orchard_bundle(Flags::ENABLED, zero, 1, 1000, BundleVersion::ironwood_v3());
474
475        let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, Some(orchard), Some(ironwood));
476
477        assert_eq!(tx.orchard_actions().count(), 2);
478        assert_eq!(tx.ironwood_actions().count(), 1);
479
480        let bytes = tx
481            .zcash_serialize_to_vec()
482            .expect("a v6 transaction with fake bundles serializes");
483        let tx2: Transaction = bytes
484            .zcash_deserialize_into()
485            .expect("a v6 transaction with fake bundles deserializes");
486
487        assert_eq!(tx.hash(), tx2.hash());
488        assert_eq!(tx2.orchard_actions().count(), 2);
489        assert_eq!(tx2.ironwood_actions().count(), 1);
490    }
491
492    /// Bundles built from different seeds must have disjoint nullifier sets, which the mempool
493    /// and state conflict tests rely on.
494    #[test]
495    fn fake_bundles_have_distinct_nullifiers() {
496        let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
497
498        // `orchard_v3` requires cross-address transfers to be disabled.
499        let a = fake_orchard_bundle(
500            Flags::CROSS_ADDRESS_DISABLED,
501            zero,
502            3,
503            0,
504            BundleVersion::orchard_v3(),
505        );
506        let b = fake_orchard_bundle(
507            Flags::CROSS_ADDRESS_DISABLED,
508            zero,
509            3,
510            100,
511            BundleVersion::orchard_v3(),
512        );
513
514        let nfs = |bundle: &Bundle<Authorized, ZatBalance>| {
515            bundle
516                .actions()
517                .iter()
518                .map(|action| action.nullifier().to_bytes())
519                .collect::<Vec<_>>()
520        };
521
522        let (a_nfs, b_nfs) = (nfs(&a), nfs(&b));
523
524        // Within a bundle.
525        let mut sorted = a_nfs.clone();
526        sorted.sort_unstable();
527        sorted.dedup();
528        assert_eq!(
529            sorted.len(),
530            3,
531            "actions in a bundle have distinct nullifiers"
532        );
533
534        // Across bundles.
535        for nf in &a_nfs {
536            assert!(
537                !b_nfs.contains(nf),
538                "bundles from different seeds must not collide"
539            );
540        }
541    }
542}