zebra_chain/transaction/arbitrary/
shielded.rs1use 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
34fn base_from_seed(seed: u64) -> pallas::Base {
39 let mut bytes = [0u8; 64];
40 bytes[..8].copy_from_slice(&seed.to_le_bytes());
41 bytes[63] = 0x01;
43 pallas::Base::from_uniform_bytes(&bytes)
44}
45
46fn 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
54fn 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
68fn fake_action(seed: u64) -> Action<redpallas::Signature<SpendAuth>> {
73 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 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
104pub 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
143pub 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 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
174const NOTE_VECTOR_SEED: u64 = 0x4E4F_5445;
176
177#[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 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
230pub 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
242pub 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 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
277pub const ACTION_WIRE_SIZE: usize = 32 * 5 + 580 + 80;
280
281pub 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
294pub 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
307pub const V5_FIRST_ACTION_RK_OFFSET: usize = (4 * 5) + 2 + 2 + 1 + 32 + 32;
314
315pub 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
335pub 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
355pub 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
369pub 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
394pub 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 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 #[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 #[test]
495 fn fake_bundles_have_distinct_nullifiers() {
496 let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
497
498 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 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 for nf in &a_nfs {
536 assert!(
537 !b_nfs.contains(nf),
538 "bundles from different seeds must not collide"
539 );
540 }
541 }
542}