1use std::{collections::HashMap, sync::Arc};
4
5use crate::methods::arrayhex;
6use chrono::{DateTime, Utc};
7use derive_getters::Getters;
8use derive_new::new;
9use hex::ToHex;
10use rand::{rand_core::UnwrapErr, rngs::SysRng};
11use zcash_script::script::Asm;
12
13use zcash_keys::address::Address;
14use zcash_primitives::transaction::{
15 builder::{BuildConfig, Builder},
16 fees::fixed::FeeRule,
17};
18use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, value::ZatBalance, value::Zatoshis};
19use zebra_chain::{
20 amount::{self, Amount, NegativeAllowed, NegativeOrZero, NonNegative},
21 block::{self, merkle::AUTH_DIGEST_PLACEHOLDER, Height},
22 parameters::{
23 subsidy::{
24 block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy,
25 nsm_fee_contribution,
26 },
27 Network, NetworkUpgrade,
28 },
29 primitives::ed25519,
30 sapling::ValueCommitment,
31 serialization::ZcashSerialize,
32 transaction::{self, SerializedTransaction, Transaction, VerifiedUnminedTx},
33 transparent::{OutPoint, Script, Utxo},
34};
35use zebra_consensus::error::TransactionError;
36use zebra_script::Sigops;
37
38use super::zec::Zec;
39use super::{super::opthex, get_block_template::MinerParams};
40
41#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
43#[serde(bound = "FeeConstraint: amount::Constraint + Clone")]
44pub struct TransactionTemplate<FeeConstraint>
45where
46 FeeConstraint: amount::Constraint + Clone + Copy,
47{
48 #[serde(with = "hex")]
50 pub(crate) data: SerializedTransaction,
51
52 #[serde(with = "hex")]
54 #[getter(copy)]
55 pub(crate) hash: transaction::Hash,
56
57 #[serde(rename = "authdigest")]
59 #[serde(with = "hex")]
60 #[getter(copy)]
61 pub(crate) auth_digest: transaction::AuthDigest,
62
63 pub(crate) depends: Vec<u16>,
70
71 #[getter(copy)]
77 pub(crate) fee: Amount<FeeConstraint>,
78
79 pub(crate) sigops: u32,
81
82 pub(crate) required: bool,
86}
87
88impl From<&VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
90 fn from(tx: &VerifiedUnminedTx) -> Self {
91 assert!(
92 !tx.transaction.transaction.is_coinbase(),
93 "unexpected coinbase transaction in mempool"
94 );
95
96 Self {
97 data: tx.transaction.transaction.as_ref().into(),
98 hash: tx.transaction.id.mined_id(),
99 auth_digest: tx
100 .transaction
101 .id
102 .auth_digest()
103 .unwrap_or(AUTH_DIGEST_PLACEHOLDER),
104
105 depends: Vec::new(),
107
108 fee: tx.miner_fee,
109
110 sigops: tx.block_sigop_count(),
113
114 required: false,
116 }
117 }
118}
119
120impl From<VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
121 fn from(tx: VerifiedUnminedTx) -> Self {
122 Self::from(&tx)
123 }
124}
125
126impl TransactionTemplate<NegativeOrZero> {
127 pub fn new_coinbase(
132 net: &Network,
133 height: Height,
134 miner_params: &MinerParams,
135 txs_fee: Amount<NonNegative>,
136 ) -> Result<Self, TransactionError> {
137 Self::new_coinbase_with_parent_pools(net, height, miner_params, txs_fee, None)
138 }
139
140 pub fn new_coinbase_with_parent_pools(
147 net: &Network,
148 height: Height,
149 miner_params: &MinerParams,
150 txs_fee: Amount<NonNegative>,
151 parent_nsm_value_balance: Option<Amount<NonNegative>>,
152 ) -> Result<Self, TransactionError> {
153 let block_subsidy = match parent_nsm_value_balance {
154 Some(parent_nsm_value_balance) => {
155 zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
156 height,
157 net,
158 parent_nsm_value_balance,
159 )?
160 }
161 None => block_subsidy(height, net)?,
162 };
163 let miner_fees = (txs_fee - nsm_fee_contribution(height, net, txs_fee))?;
165 let miner_reward = miner_subsidy(height, net, block_subsidy)? + miner_fees;
166 let miner_reward = Zatoshis::try_from(miner_reward?)?;
167
168 let mut builder = Builder::new(
169 net,
170 BlockHeight::from(height),
171 BuildConfig::Coinbase {
172 miner_data: miner_params.data().clone(),
173 },
174 );
175
176 let default_memo = MemoBytes::empty();
177 let memo = miner_params.memo().unwrap_or(&default_memo);
178
179 macro_rules! trace_err {
184 ($res:expr, $type:expr) => {
185 $res.map_err(|err| tracing::error!("Failed to add {} output: {err}", $type))
186 .ok()
187 };
188 }
189
190 let use_ironwood = NetworkUpgrade::current(net, height) >= NetworkUpgrade::Nu6_3;
196
197 let add_shielded_reward = |builder: &mut Builder<_, _>, addr: &_| {
198 let ovk = Some(::orchard::keys::OutgoingViewingKey::from([0u8; 32]));
199 if use_ironwood {
200 trace_err!(
201 builder.add_ironwood_output::<String>(ovk, *addr, miner_reward, memo.clone()),
202 "Ironwood"
203 )
204 } else {
205 trace_err!(
206 builder.add_orchard_output::<String>(ovk, *addr, miner_reward, memo.clone()),
207 "Orchard"
208 )
209 }
210 };
211
212 let add_sapling_reward = |builder: &mut Builder<_, _>, addr: &_| {
213 trace_err!(
214 builder.add_sapling_output::<String>(
215 Some(sapling_crypto::keys::OutgoingViewingKey([0u8; 32])),
216 *addr,
217 miner_reward,
218 memo.clone(),
219 ),
220 "Sapling"
221 )
222 };
223
224 let add_transparent_reward = |builder: &mut Builder<_, _>, addr| {
225 trace_err!(
226 builder.add_transparent_output(addr, miner_reward),
227 "transparent"
228 )
229 };
230
231 match miner_params.addr() {
232 Address::Unified(addr) => addr
233 .orchard()
234 .and_then(|addr| add_shielded_reward(&mut builder, addr))
235 .or_else(|| {
236 addr.sapling()
237 .and_then(|addr| add_sapling_reward(&mut builder, addr))
238 })
239 .or_else(|| {
240 addr.transparent()
241 .and_then(|addr| add_transparent_reward(&mut builder, addr))
242 }),
243
244 Address::Sapling(addr) => add_sapling_reward(&mut builder, addr),
245
246 Address::Transparent(addr) => add_transparent_reward(&mut builder, addr),
247
248 _ => Err(TransactionError::CoinbaseConstruction(
249 "Address not supported for miner rewards".to_string(),
250 ))?,
251 }
252 .ok_or(TransactionError::CoinbaseConstruction(
253 "Could not construct output with miner reward".to_string(),
254 ))?;
255
256 let mut funding_streams = funding_stream_values(height, net, block_subsidy)?
257 .into_iter()
258 .filter_map(|(receiver, amount)| {
259 Some((*funding_stream_address(height, net, receiver)?, amount))
260 })
261 .chain(net.lockbox_disbursements(height))
262 .filter_map(|(addr, amount)| {
263 Some((Zatoshis::try_from(amount).ok()?, addr.try_into().ok()?))
264 })
265 .collect::<Vec<_>>();
266
267 funding_streams.sort();
268
269 for (fs_amount, fs_addr) in funding_streams {
270 builder.add_transparent_output(&fs_addr, fs_amount)?;
271 }
272
273 let sapling_prover = zebra_consensus::sapling_prover();
274 let build_result = builder.build(
275 &Default::default(),
276 Default::default(),
277 Default::default(),
278 UnwrapErr(SysRng),
279 sapling_prover,
280 sapling_prover,
281 &FeeRule::non_standard(Zatoshis::ZERO),
282 )?;
283
284 let tx = build_result.transaction();
285 let mut data = vec![];
286 tx.write(&mut data)?;
287
288 Ok(Self {
289 data: data.into(),
290 hash: tx.txid().as_ref().into(),
291 auth_digest: tx.auth_commitment().as_ref().try_into()?,
292 depends: Vec::new(),
293 fee: (-miner_fees).constrain()?,
294 sigops: tx.sigops()?,
295 required: true,
296 })
297 }
298}
299
300#[allow(clippy::too_many_arguments)]
303#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
304pub struct TransactionObject {
305 #[serde(skip_serializing_if = "Option::is_none")]
308 #[getter(copy)]
309 pub(crate) in_active_chain: Option<bool>,
310 #[serde(with = "hex")]
312 pub(crate) hex: SerializedTransaction,
313 #[serde(skip_serializing_if = "Option::is_none")]
316 #[getter(copy)]
317 pub(crate) height: Option<i32>,
318 #[serde(skip_serializing_if = "Option::is_none")]
322 #[getter(copy)]
323 pub(crate) confirmations: Option<i64>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
329 #[getter(copy)]
330 pub(crate) fee: Option<f64>,
331
332 #[serde(rename = "vin")]
334 pub(crate) inputs: Vec<Input>,
335
336 #[serde(rename = "vout")]
338 pub(crate) outputs: Vec<Output>,
339
340 #[serde(rename = "vShieldedSpend")]
342 pub(crate) shielded_spends: Vec<ShieldedSpend>,
343
344 #[serde(rename = "vShieldedOutput")]
346 pub(crate) shielded_outputs: Vec<ShieldedOutput>,
347
348 #[serde(rename = "vjoinsplit")]
350 pub(crate) joinsplits: Vec<JoinSplit>,
351
352 #[serde(
354 skip_serializing_if = "Option::is_none",
355 with = "opthex",
356 default,
357 rename = "bindingSig"
358 )]
359 #[getter(copy)]
360 pub(crate) binding_sig: Option<[u8; 64]>,
361
362 #[serde(
364 skip_serializing_if = "Option::is_none",
365 with = "opthex",
366 default,
367 rename = "joinSplitPubKey"
368 )]
369 #[getter(copy)]
370 pub(crate) joinsplit_pub_key: Option<[u8; 32]>,
371
372 #[serde(
374 skip_serializing_if = "Option::is_none",
375 with = "opthex",
376 default,
377 rename = "joinSplitSig"
378 )]
379 #[getter(copy)]
380 pub(crate) joinsplit_sig: Option<[u8; ed25519::Signature::BYTE_SIZE]>,
381
382 #[serde(rename = "orchard", skip_serializing_if = "Option::is_none")]
384 pub(crate) orchard: Option<Orchard>,
385
386 #[serde(rename = "ironwood", skip_serializing_if = "Option::is_none")]
390 pub(crate) ironwood: Option<Orchard>,
391
392 #[serde(rename = "valueBalance", skip_serializing_if = "Option::is_none")]
394 #[getter(copy)]
395 pub(crate) value_balance: Option<f64>,
396
397 #[serde(rename = "valueBalanceZat", skip_serializing_if = "Option::is_none")]
399 #[getter(copy)]
400 pub(crate) value_balance_zat: Option<i64>,
401
402 #[serde(skip_serializing_if = "Option::is_none")]
404 #[getter(copy)]
405 pub(crate) size: Option<i64>,
406
407 #[serde(skip_serializing_if = "Option::is_none")]
409 #[getter(copy)]
410 pub(crate) time: Option<i64>,
411
412 #[serde(with = "hex")]
414 #[getter(copy)]
415 pub txid: transaction::Hash,
416
417 #[serde(
420 rename = "authdigest",
421 with = "opthex",
422 skip_serializing_if = "Option::is_none",
423 default
424 )]
425 #[getter(copy)]
426 pub(crate) auth_digest: Option<transaction::AuthDigest>,
427
428 pub(crate) overwintered: bool,
430
431 pub(crate) version: u32,
433
434 #[serde(
436 rename = "versiongroupid",
437 with = "opthex",
438 skip_serializing_if = "Option::is_none",
439 default
440 )]
441 pub(crate) version_group_id: Option<Vec<u8>>,
442
443 #[serde(rename = "locktime")]
445 pub(crate) lock_time: u32,
446
447 #[serde(rename = "expiryheight", skip_serializing_if = "Option::is_none")]
451 #[getter(copy)]
452 pub(crate) expiry_height: Option<Height>,
453
454 #[serde(
456 rename = "blockhash",
457 with = "opthex",
458 skip_serializing_if = "Option::is_none",
459 default
460 )]
461 #[getter(copy)]
462 pub(crate) block_hash: Option<block::Hash>,
463
464 #[serde(rename = "blocktime", skip_serializing_if = "Option::is_none")]
466 #[getter(copy)]
467 pub(crate) block_time: Option<i64>,
468}
469
470#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
472#[serde(untagged)]
473pub enum Input {
474 Coinbase {
476 #[serde(with = "hex")]
478 coinbase: Vec<u8>,
479 sequence: u32,
481 },
482 NonCoinbase {
484 txid: String,
486 vout: u32,
488 #[serde(rename = "scriptSig")]
490 script_sig: ScriptSig,
491 sequence: u32,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 value: Option<f64>,
496 #[serde(rename = "valueSat", skip_serializing_if = "Option::is_none")]
498 value_zat: Option<i64>,
499 #[serde(skip_serializing_if = "Option::is_none")]
501 address: Option<String>,
502 #[serde(skip_serializing_if = "Option::is_none")]
506 prevout: Option<Box<Prevout>>,
507 },
508}
509
510#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
515pub struct Prevout {
516 #[getter(copy)]
518 generated: bool,
519 #[getter(copy)]
521 height: u32,
522 #[getter(copy)]
524 value: f64,
525 #[serde(rename = "scriptPubKey")]
527 script_pub_key: ScriptPubKey,
528}
529
530#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
532pub struct Output {
533 value: f64,
535 #[serde(rename = "valueZat")]
537 value_zat: i64,
538 n: u32,
540 #[serde(rename = "scriptPubKey")]
542 script_pub_key: ScriptPubKey,
543}
544
545#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
547pub struct OutputObject {
548 #[serde(rename = "bestblock")]
549 best_block: String,
550 confirmations: u32,
551 value: f64,
552 #[serde(rename = "scriptPubKey")]
553 script_pub_key: ScriptPubKey,
554 version: u32,
555 coinbase: bool,
556}
557impl OutputObject {
558 pub fn from_output(
559 output: &zebra_chain::transparent::Output,
560 best_block: String,
561 confirmations: u32,
562 version: u32,
563 coinbase: bool,
564 network: &Network,
565 ) -> Self {
566 Self {
567 best_block,
568 confirmations,
569 value: crate::methods::types::zec::Zec::from(output.value()).lossy_zec(),
570 script_pub_key: ScriptPubKey::from_output(output, network),
571 version,
572 coinbase,
573 }
574 }
575}
576
577#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
579pub struct ScriptPubKey {
580 asm: String,
582 #[serde(with = "hex")]
584 hex: Script,
585 #[serde(rename = "reqSigs")]
587 #[serde(default)]
588 #[serde(skip_serializing_if = "Option::is_none")]
589 #[getter(copy)]
590 req_sigs: Option<u32>,
591 r#type: String,
593 #[serde(default)]
595 #[serde(skip_serializing_if = "Option::is_none")]
596 addresses: Option<Vec<String>>,
597}
598
599impl ScriptPubKey {
600 pub fn from_output(output: &zebra_chain::transparent::Output, network: &Network) -> Self {
602 let lock_script = &output.lock_script;
603 let addresses = output.address(network).map(|addr| vec![addr.to_string()]);
604 let req_sigs = addresses.as_ref().map(|a| a.len() as u32);
605
606 Self {
607 asm: zcash_script::script::Code(lock_script.as_raw_bytes().to_vec()).to_asm(false),
610 hex: lock_script.clone(),
611 req_sigs,
612 r#type: zcash_script::script::Code(lock_script.as_raw_bytes().to_vec())
613 .to_component()
614 .ok()
615 .and_then(|c| c.refine().ok())
616 .and_then(|component| zcash_script::solver::standard(&component))
617 .map(|kind| match kind {
618 zcash_script::solver::ScriptKind::PubKeyHash { .. } => "pubkeyhash",
619 zcash_script::solver::ScriptKind::ScriptHash { .. } => "scripthash",
620 zcash_script::solver::ScriptKind::MultiSig { .. } => "multisig",
621 zcash_script::solver::ScriptKind::NullData { .. } => "nulldata",
622 zcash_script::solver::ScriptKind::PubKey { .. } => "pubkey",
623 })
624 .unwrap_or("nonstandard")
625 .to_string(),
626 addresses,
627 }
628 }
629}
630
631#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
633pub struct ScriptSig {
634 asm: String,
636 hex: Script,
638}
639
640#[allow(clippy::too_many_arguments)]
642#[serde_with::serde_as]
643#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
644pub struct JoinSplit {
645 #[serde(rename = "vpub_old")]
647 old_public_value: f64,
648 #[serde(rename = "vpub_oldZat")]
650 old_public_value_zat: i64,
651 #[serde(rename = "vpub_new")]
653 new_public_value: f64,
654 #[serde(rename = "vpub_newZat")]
656 new_public_value_zat: i64,
657 #[serde(with = "hex")]
659 #[getter(copy)]
660 anchor: [u8; 32],
661 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
663 nullifiers: Vec<[u8; 32]>,
664 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
666 commitments: Vec<[u8; 32]>,
667 #[serde(rename = "onetimePubKey")]
669 #[serde(with = "hex")]
670 #[getter(copy)]
671 one_time_pubkey: [u8; 32],
672 #[serde(rename = "randomSeed")]
674 #[serde(with = "hex")]
675 #[getter(copy)]
676 random_seed: [u8; 32],
677 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
679 macs: Vec<[u8; 32]>,
680 #[serde(with = "hex")]
682 proof: Vec<u8>,
683 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
685 ciphertexts: Vec<Vec<u8>>,
686}
687
688#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
690pub struct ShieldedSpend {
691 #[serde(with = "hex")]
693 #[getter(skip)]
694 cv: ValueCommitment,
695 #[serde(with = "hex")]
697 #[getter(copy)]
698 anchor: [u8; 32],
699 #[serde(with = "hex")]
701 #[getter(copy)]
702 nullifier: [u8; 32],
703 #[serde(with = "hex")]
705 #[getter(copy)]
706 rk: [u8; 32],
707 #[serde(with = "hex")]
709 #[getter(copy)]
710 proof: [u8; 192],
711 #[serde(rename = "spendAuthSig", with = "hex")]
713 #[getter(copy)]
714 spend_auth_sig: [u8; 64],
715}
716
717impl ShieldedSpend {
719 pub fn cv(&self) -> ValueCommitment {
721 self.cv.clone()
722 }
723}
724
725#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
727pub struct ShieldedOutput {
728 #[serde(with = "hex")]
730 #[getter(skip)]
731 cv: ValueCommitment,
732 #[serde(rename = "cmu", with = "hex")]
734 cm_u: [u8; 32],
735 #[serde(rename = "ephemeralKey", with = "hex")]
737 ephemeral_key: [u8; 32],
738 #[serde(rename = "encCiphertext", with = "arrayhex")]
740 enc_ciphertext: [u8; 580],
741 #[serde(rename = "outCiphertext", with = "hex")]
743 out_ciphertext: [u8; 80],
744 #[serde(with = "hex")]
746 proof: [u8; 192],
747}
748
749impl ShieldedOutput {
751 pub fn cv(&self) -> ValueCommitment {
753 self.cv.clone()
754 }
755}
756
757#[serde_with::serde_as]
759#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
760pub struct Orchard {
761 actions: Vec<OrchardAction>,
763 #[serde(rename = "valueBalance")]
765 value_balance: f64,
766 #[serde(rename = "valueBalanceZat")]
768 value_balance_zat: i64,
769 #[serde(skip_serializing_if = "Option::is_none")]
771 flags: Option<OrchardFlags>,
772 #[serde_as(as = "Option<serde_with::hex::Hex>")]
774 #[serde(skip_serializing_if = "Option::is_none")]
775 #[getter(copy)]
776 anchor: Option<[u8; 32]>,
777 #[serde_as(as = "Option<serde_with::hex::Hex>")]
779 #[serde(skip_serializing_if = "Option::is_none")]
780 proof: Option<Vec<u8>>,
781 #[serde(rename = "bindingSig")]
783 #[serde(skip_serializing_if = "Option::is_none")]
784 #[serde_as(as = "Option<serde_with::hex::Hex>")]
785 #[getter(copy)]
786 binding_sig: Option<[u8; 64]>,
787}
788
789#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
791pub struct OrchardFlags {
792 #[serde(rename = "enableOutputs")]
794 enable_outputs: bool,
795 #[serde(rename = "enableSpends")]
797 enable_spends: bool,
798}
799
800#[allow(clippy::too_many_arguments)]
802#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
803pub struct OrchardAction {
804 #[serde(with = "hex")]
806 cv: [u8; 32],
807 #[serde(with = "hex")]
809 nullifier: [u8; 32],
810 #[serde(with = "hex")]
812 rk: [u8; 32],
813 #[serde(rename = "cmx", with = "hex")]
815 cm_x: [u8; 32],
816 #[serde(rename = "ephemeralKey", with = "hex")]
818 ephemeral_key: [u8; 32],
819 #[serde(rename = "encCiphertext", with = "arrayhex")]
821 enc_ciphertext: [u8; 580],
822 #[serde(rename = "spendAuthSig", with = "hex")]
824 spend_auth_sig: [u8; 64],
825 #[serde(rename = "outCiphertext", with = "hex")]
827 out_ciphertext: [u8; 80],
828}
829
830fn orchard_shaped_object(
841 bundle: Option<&::orchard::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
842 value_balance: Amount<NegativeAllowed>,
843) -> Orchard {
844 let actions = bundle
845 .into_iter()
846 .flat_map(|bundle| bundle.actions().iter())
847 .map(|action| OrchardAction {
848 cv: action.cv_net().to_bytes(),
849 nullifier: action.nullifier().to_bytes(),
850 rk: action.rk().into(),
851 cm_x: action.cmx().to_bytes(),
852 ephemeral_key: action.encrypted_note().epk_bytes,
853 enc_ciphertext: action.encrypted_note().enc_ciphertext.0,
854 spend_auth_sig: action.authorization().into(),
855 out_ciphertext: action.encrypted_note().out_ciphertext,
856 })
857 .collect();
858
859 Orchard {
860 actions,
861 value_balance: Zec::from(value_balance).lossy_zec(),
862 value_balance_zat: value_balance.zatoshis(),
863 flags: bundle.map(|bundle| {
864 let flags = bundle.flags();
865 OrchardFlags::new(flags.outputs_enabled(), flags.spends_enabled())
866 }),
867 anchor: bundle.map(|bundle| {
868 let mut anchor = bundle.anchor().to_bytes();
869 anchor.reverse();
871 anchor
872 }),
873 proof: bundle.map(|bundle| bundle.authorization().proof().as_ref().to_vec()),
874 binding_sig: bundle
875 .map(|bundle| <[u8; 64]>::from(bundle.authorization().binding_signature())),
876 }
877}
878
879impl Default for TransactionObject {
880 fn default() -> Self {
881 Self {
882 hex: SerializedTransaction::from(
883 [0u8; zebra_chain::transaction::MIN_TRANSPARENT_TX_SIZE as usize].to_vec(),
884 ),
885 height: Option::default(),
886 confirmations: Option::default(),
887 fee: None,
888 inputs: Vec::new(),
889 outputs: Vec::new(),
890 shielded_spends: Vec::new(),
891 shielded_outputs: Vec::new(),
892 joinsplits: Vec::new(),
893 orchard: None,
894 ironwood: None,
895 binding_sig: None,
896 joinsplit_pub_key: None,
897 joinsplit_sig: None,
898 value_balance: None,
899 value_balance_zat: None,
900 size: None,
901 time: None,
902 txid: transaction::Hash::from([0u8; 32]),
903 in_active_chain: None,
904 auth_digest: None,
905 overwintered: false,
906 version: 4,
907 version_group_id: None,
908 lock_time: 0,
909 expiry_height: None,
910 block_hash: None,
911 block_time: None,
912 }
913 }
914}
915
916impl TransactionObject {
917 #[allow(clippy::unwrap_in_result)]
919 #[allow(clippy::too_many_arguments)]
920 pub fn from_transaction(
921 tx: Arc<Transaction>,
922 height: Option<block::Height>,
923 confirmations: Option<i64>,
924 network: &Network,
925 block_time: Option<DateTime<Utc>>,
926 block_hash: Option<block::Hash>,
927 in_active_chain: Option<bool>,
928 txid: transaction::Hash,
929 ) -> Self {
930 let block_time = block_time.map(|bt| bt.timestamp());
931 Self {
932 hex: tx.clone().into(),
933 height: if in_active_chain.unwrap_or_default() {
934 height.map(|height| height.0 as i32)
935 } else if block_hash.is_some() {
936 Some(-1)
938 } else {
939 None
941 },
942 confirmations: if in_active_chain.unwrap_or_default() {
943 confirmations
944 } else if block_hash.is_some() {
945 Some(0)
947 } else {
948 None
950 },
951 fee: None,
953 inputs: tx
954 .inputs()
955 .iter()
956 .map(|input| match input {
957 zebra_chain::transparent::Input::Coinbase { sequence, .. } => Input::Coinbase {
958 coinbase: input
959 .coinbase_script()
960 .expect("we know it is a valid coinbase script"),
961 sequence: *sequence,
962 },
963 zebra_chain::transparent::Input::PrevOut {
964 sequence,
965 unlock_script,
966 outpoint,
967 } => Input::NonCoinbase {
968 txid: outpoint.hash.encode_hex(),
969 vout: outpoint.index,
970 script_sig: ScriptSig {
971 asm: zcash_script::script::Code(unlock_script.as_raw_bytes().to_vec())
973 .to_asm(true),
974 hex: unlock_script.clone(),
975 },
976 sequence: *sequence,
977 value: None,
978 value_zat: None,
979 address: None,
980 prevout: None,
981 },
982 })
983 .collect(),
984 outputs: tx
985 .outputs()
986 .iter()
987 .enumerate()
988 .map(|(n, output)| Output {
989 value: Zec::from(output.value).lossy_zec(),
990 value_zat: output.value.zatoshis(),
991 n: n as u32,
992 script_pub_key: ScriptPubKey::from_output(output, network),
993 })
994 .collect(),
995 shielded_spends: tx
996 .sapling_spends()
997 .map(|spend| {
998 let mut anchor = spend.anchor().to_bytes();
999 anchor.reverse();
1000
1001 let mut nullifier = spend.nullifier().0;
1002 nullifier.reverse();
1003
1004 let mut rk: [u8; 32] = (*spend.rk()).into();
1005 rk.reverse();
1006
1007 let spend_auth_sig: [u8; 64] = (*spend.spend_auth_sig()).into();
1008
1009 ShieldedSpend {
1010 cv: ValueCommitment(spend.cv().clone()),
1011 anchor,
1012 nullifier,
1013 rk,
1014 proof: *spend.zkproof(),
1015 spend_auth_sig,
1016 }
1017 })
1018 .collect(),
1019 shielded_outputs: tx
1020 .sapling_outputs()
1021 .map(|output| {
1022 let mut cm_u: [u8; 32] = output.cmu().to_bytes();
1023 cm_u.reverse();
1024 let mut ephemeral_key: [u8; 32] = output.ephemeral_key().0;
1025 ephemeral_key.reverse();
1026 let enc_ciphertext: [u8; 580] = *output.enc_ciphertext();
1027 let out_ciphertext: [u8; 80] = *output.out_ciphertext();
1028
1029 ShieldedOutput {
1030 cv: ValueCommitment(output.cv().clone()),
1031 cm_u,
1032 ephemeral_key,
1033 enc_ciphertext,
1034 out_ciphertext,
1035 proof: *output.zkproof(),
1036 }
1037 })
1038 .collect(),
1039 joinsplits: tx
1040 .sprout_joinsplit_descriptions()
1041 .map(|joinsplit| {
1042 let display_order = |bytes: &[u8; 32]| {
1045 let mut bytes = *bytes;
1046 bytes.reverse();
1047 bytes
1048 };
1049
1050 let (ephemeral_key, proof, ciphertexts) =
1051 transaction::sprout_joinsplit_key_proof_and_ciphertexts(joinsplit);
1052
1053 let vpub_old = i64::from(joinsplit.vpub_old());
1054 let vpub_new = i64::from(joinsplit.vpub_new());
1055 let vpub_old_amount = Amount::<NonNegative>::try_from(vpub_old)
1056 .expect("vpub_old is a valid non-negative amount");
1057 let vpub_new_amount = Amount::<NonNegative>::try_from(vpub_new)
1058 .expect("vpub_new is a valid non-negative amount");
1059
1060 JoinSplit {
1061 old_public_value: Zec::from(vpub_old_amount).lossy_zec(),
1062 old_public_value_zat: vpub_old,
1063 new_public_value: Zec::from(vpub_new_amount).lossy_zec(),
1064 new_public_value_zat: vpub_new,
1065 anchor: display_order(joinsplit.anchor()),
1066 nullifiers: joinsplit.nullifiers().iter().map(display_order).collect(),
1067 commitments: joinsplit.commitments().iter().map(display_order).collect(),
1068 one_time_pubkey: display_order(&ephemeral_key),
1069 random_seed: display_order(joinsplit.random_seed()),
1070 macs: joinsplit.macs().iter().map(display_order).collect(),
1071 proof,
1072 ciphertexts: ciphertexts.iter().map(|c| c.to_vec()).collect(),
1073 }
1074 })
1075 .collect(),
1076 value_balance: Some(Zec::from(tx.sapling_value_balance().sapling_amount()).lossy_zec()),
1077 value_balance_zat: Some(tx.sapling_value_balance().sapling_amount().zatoshis()),
1078 orchard: Some(orchard_shaped_object(
1079 tx.orchard_bundle(),
1080 tx.orchard_value_balance().orchard_amount(),
1081 )),
1082 ironwood: tx.ironwood_bundle().map(|bundle| {
1083 orchard_shaped_object(Some(bundle), tx.ironwood_value_balance().ironwood_amount())
1084 }),
1085 binding_sig: tx.sapling_bundle().map(|b| {
1086 let sig: [u8; 64] = b.authorization().binding_sig.into();
1087 sig
1088 }),
1089 joinsplit_pub_key: tx.sprout_joinsplit_pub_key().map(|raw_key| {
1090 let mut key: [u8; 32] = raw_key.into();
1092 key.reverse();
1093 key
1094 }),
1095 joinsplit_sig: tx.sprout_bundle().map(|b| b.joinsplit_sig),
1096 size: tx.zcash_serialized_size().try_into().ok(),
1097 time: block_time,
1098 txid,
1099 in_active_chain,
1100 auth_digest: tx.auth_digest(),
1101 overwintered: tx.is_overwintered(),
1102 version: tx.version(),
1103 version_group_id: tx.version_group_id().map(|id| id.to_be_bytes().to_vec()),
1104 lock_time: tx.raw_lock_time(),
1105 expiry_height: if tx.is_overwintered() {
1108 Some(tx.expiry_height().unwrap_or(Height(0)))
1109 } else {
1110 None
1111 },
1112 block_hash,
1113 block_time,
1114 }
1115 }
1116
1117 pub fn add_prevouts(
1125 &mut self,
1126 tx: &Transaction,
1127 spent_utxos: &HashMap<OutPoint, Utxo>,
1128 network: &Network,
1129 ) {
1130 if tx.is_coinbase() {
1131 return;
1132 }
1133
1134 for (input, source_input) in self.inputs.iter_mut().zip(tx.inputs()) {
1139 let Input::NonCoinbase { prevout, .. } = input else {
1140 continue;
1141 };
1142 let Some(utxo) = source_input.outpoint().and_then(|o| spent_utxos.get(&o)) else {
1143 continue;
1144 };
1145 *prevout = Some(Box::new(Prevout::new(
1146 utxo.from_coinbase,
1147 utxo.height.0,
1148 Zec::from(utxo.output.value).lossy_zec(),
1149 ScriptPubKey::from_output(&utxo.output, network),
1150 )));
1151 }
1152
1153 if tx
1155 .spent_outpoints()
1156 .all(|outpoint| spent_utxos.contains_key(&outpoint))
1157 {
1158 if let Ok(value_balance) = tx.value_balance(spent_utxos) {
1159 if let Ok(fee) = value_balance.remaining_transaction_value() {
1160 self.fee = Some(Zec::from(fee).lossy_zec());
1161 }
1162 }
1163 }
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170
1171 use zebra_chain::{block::Block, serialization::ZcashDeserializeInto, transparent::Output};
1172
1173 fn first_transparent_only_tx() -> Arc<Transaction> {
1178 zebra_test::vectors::MAINNET_BLOCKS
1179 .values()
1180 .flat_map(|bytes| {
1181 let block: Block = bytes
1182 .zcash_deserialize_into()
1183 .expect("hard-coded test vector must deserialize");
1184 block.transactions.clone()
1185 })
1186 .find(|tx| {
1187 !tx.is_coinbase()
1188 && !tx.inputs().is_empty()
1189 && tx.sapling_spends_count() == 0
1190 && tx.sapling_outputs().count() == 0
1191 && tx.orchard_actions().count() == 0
1192 && tx.ironwood_actions().count() == 0
1193 && tx.sprout_joinsplit_descriptions().next().is_none()
1194 })
1195 .expect("the mainnet block vectors contain a transparent-only transaction")
1196 }
1197
1198 #[test]
1201 fn add_prevouts_fills_prevouts_and_fee() {
1202 let network = Network::Mainnet;
1203 let tx = first_transparent_only_tx();
1204
1205 let output_total = tx
1206 .outputs()
1207 .iter()
1208 .try_fold(Amount::<NonNegative>::zero(), |acc, output| {
1209 acc + output.value
1210 })
1211 .expect("transparent outputs sum within range");
1212
1213 const FEE: i64 = 10_000;
1215 let first_input_value = (output_total + Amount::<NonNegative>::try_from(FEE).unwrap())
1216 .expect("input total within range");
1217 let lock_script = Script::new(&[0x76, 0xa9]);
1218
1219 let mut spent_utxos = HashMap::new();
1220 for (i, outpoint) in tx.spent_outpoints().enumerate() {
1221 let value = if i == 0 {
1222 first_input_value
1223 } else {
1224 Amount::zero()
1225 };
1226 spent_utxos.insert(
1227 outpoint,
1228 Utxo::new(Output::new(value, lock_script.clone()), Height(123), i == 0),
1229 );
1230 }
1231
1232 let mut object = TransactionObject::from_transaction(
1233 tx.clone(),
1234 None,
1235 None,
1236 &network,
1237 None,
1238 None,
1239 None,
1240 tx.hash(),
1241 );
1242 object.add_prevouts(&tx, &spent_utxos, &network);
1243
1244 for input in &object.inputs {
1246 let Input::NonCoinbase { prevout, .. } = input else {
1247 panic!("a transparent-only transaction has only non-coinbase inputs");
1248 };
1249 assert!(prevout.is_some(), "each input must carry a prevout");
1250 }
1251
1252 let Input::NonCoinbase { prevout, .. } = &object.inputs[0] else {
1254 unreachable!("checked above");
1255 };
1256 let prevout = prevout.as_ref().expect("prevout is set");
1257 assert!(prevout.generated());
1258 assert_eq!(prevout.height(), 123);
1259 assert_eq!(prevout.value(), Zec::from(first_input_value).lossy_zec());
1260
1261 let expected_fee = Zec::from(Amount::<NonNegative>::try_from(FEE).unwrap()).lossy_zec();
1263 assert_eq!(object.fee(), Some(expected_fee));
1264 }
1265
1266 #[test]
1268 fn add_prevouts_is_noop_for_coinbase() {
1269 let network = Network::Mainnet;
1270 let block: Block = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
1271 .zcash_deserialize_into()
1272 .expect("hard-coded test vector must deserialize");
1273 let coinbase = block.transactions[0].clone();
1274 assert!(coinbase.is_coinbase());
1275
1276 let mut object = TransactionObject::from_transaction(
1277 coinbase.clone(),
1278 None,
1279 None,
1280 &network,
1281 None,
1282 None,
1283 None,
1284 coinbase.hash(),
1285 );
1286 object.add_prevouts(&coinbase, &HashMap::new(), &network);
1287
1288 assert_eq!(object.fee(), None);
1289 assert!(matches!(object.inputs[0], Input::Coinbase { .. }));
1290 }
1291
1292 #[test]
1295 fn add_prevouts_omits_fee_when_a_spent_output_is_missing() {
1296 let network = Network::Mainnet;
1297 let tx = first_transparent_only_tx();
1298
1299 let mut object = TransactionObject::from_transaction(
1300 tx.clone(),
1301 None,
1302 None,
1303 &network,
1304 None,
1305 None,
1306 None,
1307 tx.hash(),
1308 );
1309 object.add_prevouts(&tx, &HashMap::new(), &network);
1311
1312 assert_eq!(object.fee(), None);
1313 for input in &object.inputs {
1314 let Input::NonCoinbase { prevout, .. } = input else {
1315 panic!("a transparent-only transaction has only non-coinbase inputs");
1316 };
1317 assert!(
1318 prevout.is_none(),
1319 "a missing spent output must leave no prevout"
1320 );
1321 }
1322 }
1323
1324 #[test]
1330 fn vjoinsplit_is_populated_for_sprout_transactions() {
1331 let block: Block = zebra_test::vectors::BLOCK_MAINNET_419201_BYTES
1332 .zcash_deserialize_into()
1333 .expect("hard-coded test vector must deserialize");
1334
1335 let tx = block
1336 .transactions
1337 .iter()
1338 .find(|tx| tx.sprout_joinsplit_descriptions().next().is_some())
1339 .expect("block 419,201 contains a transaction with JoinSplits")
1340 .clone();
1341
1342 let expected_count = tx.sprout_joinsplit_descriptions().count();
1343 assert!(expected_count > 0);
1344
1345 let object = TransactionObject::from_transaction(
1346 tx.clone(),
1347 None,
1348 None,
1349 &Network::Mainnet,
1350 None,
1351 None,
1352 None,
1353 tx.hash(),
1354 );
1355
1356 assert_eq!(
1357 object.joinsplits.len(),
1358 expected_count,
1359 "every JoinSplit must appear in vjoinsplit",
1360 );
1361
1362 let joinsplit = tx
1364 .sprout_joinsplit_descriptions()
1365 .next()
1366 .expect("the transaction has JoinSplits");
1367 let rendered = &object.joinsplits[0];
1368
1369 let reversed = |bytes: &[u8; 32]| {
1370 let mut bytes = *bytes;
1371 bytes.reverse();
1372 bytes
1373 };
1374
1375 assert_eq!(rendered.anchor, reversed(joinsplit.anchor()));
1376 assert_eq!(rendered.nullifiers[0], reversed(&joinsplit.nullifiers()[0]));
1377 assert_eq!(
1378 rendered.commitments[0],
1379 reversed(&joinsplit.commitments()[0])
1380 );
1381 assert_eq!(rendered.random_seed, reversed(joinsplit.random_seed()));
1382 assert_eq!(rendered.macs[0], reversed(&joinsplit.macs()[0]));
1383 assert_eq!(
1384 rendered.old_public_value_zat,
1385 i64::from(joinsplit.vpub_old())
1386 );
1387 assert_eq!(
1388 rendered.new_public_value_zat,
1389 i64::from(joinsplit.vpub_new())
1390 );
1391
1392 assert_eq!(rendered.ciphertexts.len(), 2);
1394 assert!(rendered.ciphertexts.iter().all(|c| c.len() == 601));
1395 assert_ne!(
1396 rendered.one_time_pubkey, [0u8; 32],
1397 "the ephemeral key must be read from the JoinSplit, not left zeroed",
1398 );
1399
1400 assert_eq!(rendered.proof.len(), 192);
1402 }
1403
1404 #[test]
1411 fn vjoinsplit_proof_is_populated_for_phgr13_transactions() {
1412 const PHGR_PROOF_SIZE: usize = 296;
1413
1414 let block: Block = zebra_test::vectors::BLOCK_TESTNET_141042_BYTES
1415 .zcash_deserialize_into()
1416 .expect("hard-coded test vector must deserialize");
1417
1418 let tx = block
1419 .transactions
1420 .iter()
1421 .find(|tx| tx.sprout_joinsplit_descriptions().next().is_some())
1422 .expect("block 141,042 contains transactions with JoinSplits")
1423 .clone();
1424
1425 let joinsplit = tx
1426 .sprout_joinsplit_descriptions()
1427 .next()
1428 .expect("the transaction has JoinSplits");
1429
1430 assert!(
1433 joinsplit.groth_proof_bytes().is_none(),
1434 "the fixture must exercise a PHGR13 proof, not a Groth16 one",
1435 );
1436
1437 let object = TransactionObject::from_transaction(
1438 tx.clone(),
1439 None,
1440 None,
1441 &Network::new_default_testnet(),
1442 None,
1443 None,
1444 None,
1445 tx.hash(),
1446 );
1447
1448 assert_eq!(object.joinsplits.len(), 1);
1449 let rendered = &object.joinsplits[0];
1450
1451 assert_eq!(
1452 rendered.proof.len(),
1453 PHGR_PROOF_SIZE,
1454 "the PHGR13 proof must be rendered, not replaced with an empty vector",
1455 );
1456
1457 let raw = tx
1462 .zcash_serialize_to_vec()
1463 .expect("a parsed transaction re-serializes");
1464 let proof_offset = raw
1465 .windows(PHGR_PROOF_SIZE)
1466 .position(|window| window == rendered.proof.as_slice())
1467 .expect("the rendered proof must appear verbatim in the wire encoding");
1468
1469 assert_eq!(
1470 &raw[proof_offset - 32..proof_offset],
1471 &joinsplit.macs()[1][..],
1472 "the proof must be read from immediately after `vmacs[1]`",
1473 );
1474 assert_eq!(
1475 &raw[proof_offset + PHGR_PROOF_SIZE..proof_offset + PHGR_PROOF_SIZE + 601],
1476 rendered.ciphertexts[0].as_slice(),
1477 "the proof must be read from immediately before the first ciphertext",
1478 );
1479 }
1480
1481 #[test]
1483 fn vjoinsplit_is_empty_for_coinbase() {
1484 let block: Block = zebra_test::vectors::BLOCK_MAINNET_419201_BYTES
1485 .zcash_deserialize_into()
1486 .expect("hard-coded test vector must deserialize");
1487
1488 let coinbase = block.transactions[0].clone();
1489 assert!(coinbase.is_coinbase());
1490
1491 let object = TransactionObject::from_transaction(
1492 coinbase.clone(),
1493 None,
1494 None,
1495 &Network::Mainnet,
1496 None,
1497 None,
1498 None,
1499 coinbase.hash(),
1500 );
1501
1502 assert!(object.joinsplits.is_empty());
1503 assert!(TransactionObject::default().joinsplits.is_empty());
1504 }
1505}