Skip to main content

zebra_rpc/methods/types/
transaction.rs

1//! Transaction-related types.
2
3use 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/// Transaction data and fields needed to generate blocks using the `getblocktemplate` RPC.
42#[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    /// The hex-encoded serialized data for this transaction.
49    #[serde(with = "hex")]
50    pub(crate) data: SerializedTransaction,
51
52    /// The transaction ID of this transaction.
53    #[serde(with = "hex")]
54    #[getter(copy)]
55    pub(crate) hash: transaction::Hash,
56
57    /// The authorizing data digest of a v5 transaction, or a placeholder for older versions.
58    #[serde(rename = "authdigest")]
59    #[serde(with = "hex")]
60    #[getter(copy)]
61    pub(crate) auth_digest: transaction::AuthDigest,
62
63    /// The transactions in this block template that this transaction depends upon.
64    /// These are 1-based indexes in the `transactions` list.
65    ///
66    /// Populated when the selected transactions are assembled into a block template.
67    ///
68    /// We use `u16` because 2 MB blocks are limited to around 39,000 transactions.
69    pub(crate) depends: Vec<u16>,
70
71    /// The fee for this transaction.
72    ///
73    /// Non-coinbase transactions must be `NonNegative`.
74    /// The coinbase transaction `fee` is the negative total collected miner fees, excluding
75    /// the NSM contribution, so its fee must be `NegativeOrZero`.
76    #[getter(copy)]
77    pub(crate) fee: Amount<FeeConstraint>,
78
79    /// The number of transparent signature operations in this transaction.
80    pub(crate) sigops: u32,
81
82    /// Is this transaction required in the block?
83    ///
84    /// Coinbase transactions are required, all other transactions are not.
85    pub(crate) required: bool,
86}
87
88// Convert from a mempool transaction to a non-coinbase transaction template.
89impl 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            // Indexes require the final selected transaction order.
106            depends: Vec::new(),
107
108            fee: tx.miner_fee,
109
110            // Report the full block-level sigop count (legacy + P2SH) so the template `sigops`
111            // field matches what the block verifier charges against `MAX_BLOCK_SIGOPS`.
112            sigops: tx.block_sigop_count(),
113
114            // Zebra does not require any transactions except the coinbase transaction.
115            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    /// Constructs a transaction template for a coinbase without parent-dependent reissuance.
128    ///
129    /// `txs_fee` is the gross transaction fee total. The payout and the template's negative
130    /// `fee` include only the miner's share after the NSM contribution.
131    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    /// Constructs a transaction template for a coinbase transaction in a block whose parent leaves
141    /// `parent_nsm_value_balance` in the NSM value balance.
142    ///
143    /// The parent's NSM value balance is required from the NSM reissuance height, because it
144    /// determines the block subsidy.
145    /// The payout and negative `fee` include only net miner fees after the NSM contribution.
146    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        // From NU7 activation, the coinbase can't claim the fees ZIP 235 removes from circulation.
164        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        // ZIP-233 was dropped from the v6 transaction format, so no burn amount is set here. The
180        // fees ZIP 235 removes from circulation are left out of the miner reward above, and need
181        // no transaction field.
182
183        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        // On NU6.3 onward the coinbase MUST have an empty Orchard component, and newly shielded
191        // coinbase value is routed to the Ironwood pool instead (see the Ironwood pool spec and
192        // `coinbase_orchard_component_empty` in zebra-consensus). Ironwood outputs use the same
193        // Orchard-shaped `orchard::Address` as their recipient, so a unified miner address with an
194        // Orchard receiver just gets routed to the Ironwood output builder from NU6.3 onward.
195        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/// A Transaction object as returned by `getrawtransaction` and `getblock` RPC
301/// requests.
302#[allow(clippy::too_many_arguments)]
303#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
304pub struct TransactionObject {
305    /// Whether specified block is in the active chain or not (only present with
306    /// explicit "blockhash" argument)
307    #[serde(skip_serializing_if = "Option::is_none")]
308    #[getter(copy)]
309    pub(crate) in_active_chain: Option<bool>,
310    /// The raw transaction, encoded as hex bytes.
311    #[serde(with = "hex")]
312    pub(crate) hex: SerializedTransaction,
313    /// The height of the block in the best chain that contains the tx, -1 if
314    /// it's in a side chain block, or `None` if the tx is in the mempool.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    #[getter(copy)]
317    pub(crate) height: Option<i32>,
318    /// The height diff between the block containing the tx and the best chain
319    /// tip + 1, 0 if it's in a side chain, or `None` if the tx is in the
320    /// mempool.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    #[getter(copy)]
323    pub(crate) confirmations: Option<i64>,
324
325    /// The transaction fee in ZEC, only present at `getblock` verbosity 3.
326    ///
327    /// Omitted for the coinbase transaction and when a spent output cannot be found.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    #[getter(copy)]
330    pub(crate) fee: Option<f64>,
331
332    /// Transparent inputs of the transaction.
333    #[serde(rename = "vin")]
334    pub(crate) inputs: Vec<Input>,
335
336    /// Transparent outputs of the transaction.
337    #[serde(rename = "vout")]
338    pub(crate) outputs: Vec<Output>,
339
340    /// Sapling spends of the transaction.
341    #[serde(rename = "vShieldedSpend")]
342    pub(crate) shielded_spends: Vec<ShieldedSpend>,
343
344    /// Sapling outputs of the transaction.
345    #[serde(rename = "vShieldedOutput")]
346    pub(crate) shielded_outputs: Vec<ShieldedOutput>,
347
348    /// Transparent outputs of the transaction.
349    #[serde(rename = "vjoinsplit")]
350    pub(crate) joinsplits: Vec<JoinSplit>,
351
352    /// Sapling binding signature of the transaction.
353    #[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    /// JoinSplit public key of the transaction.
363    #[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    /// JoinSplit signature of the transaction.
373    #[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    /// Orchard actions of the transaction.
383    #[serde(rename = "orchard", skip_serializing_if = "Option::is_none")]
384    pub(crate) orchard: Option<Orchard>,
385
386    /// Ironwood actions of the transaction (v6 transactions from NU6.3 onward).
387    ///
388    /// The Ironwood pool reuses the Orchard-shaped bundle, so this uses the same [`Orchard`] object.
389    #[serde(rename = "ironwood", skip_serializing_if = "Option::is_none")]
390    pub(crate) ironwood: Option<Orchard>,
391
392    /// The net value of Sapling Spends minus Outputs in ZEC
393    #[serde(rename = "valueBalance", skip_serializing_if = "Option::is_none")]
394    #[getter(copy)]
395    pub(crate) value_balance: Option<f64>,
396
397    /// The net value of Sapling Spends minus Outputs in zatoshis
398    #[serde(rename = "valueBalanceZat", skip_serializing_if = "Option::is_none")]
399    #[getter(copy)]
400    pub(crate) value_balance_zat: Option<i64>,
401
402    /// The size of the transaction in bytes.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    #[getter(copy)]
405    pub(crate) size: Option<i64>,
406
407    /// The time the transaction was included in a block.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    #[getter(copy)]
410    pub(crate) time: Option<i64>,
411
412    /// The transaction identifier, encoded as hex bytes.
413    #[serde(with = "hex")]
414    #[getter(copy)]
415    pub txid: transaction::Hash,
416
417    /// The transaction's auth digest. For pre-v5 transactions this will be
418    /// ffff..ffff
419    #[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    /// Whether the overwintered flag is set
429    pub(crate) overwintered: bool,
430
431    /// The version of the transaction.
432    pub(crate) version: u32,
433
434    /// The version group ID.
435    #[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    /// The lock time
444    #[serde(rename = "locktime")]
445    pub(crate) lock_time: u32,
446
447    /// The block height after which the transaction expires.
448    /// Included for Overwinter+ transactions (matching zcashd), omitted for V1/V2.
449    /// See: <https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L224-L226>
450    #[serde(rename = "expiryheight", skip_serializing_if = "Option::is_none")]
451    #[getter(copy)]
452    pub(crate) expiry_height: Option<Height>,
453
454    /// The block hash
455    #[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    /// The block height after which the transaction expires
465    #[serde(rename = "blocktime", skip_serializing_if = "Option::is_none")]
466    #[getter(copy)]
467    pub(crate) block_time: Option<i64>,
468}
469
470/// The transparent input of a transaction.
471#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
472#[serde(untagged)]
473pub enum Input {
474    /// A coinbase input.
475    Coinbase {
476        /// The coinbase scriptSig as hex.
477        #[serde(with = "hex")]
478        coinbase: Vec<u8>,
479        /// The script sequence number.
480        sequence: u32,
481    },
482    /// A non-coinbase input.
483    NonCoinbase {
484        /// The transaction id.
485        txid: String,
486        /// The vout index.
487        vout: u32,
488        /// The script.
489        #[serde(rename = "scriptSig")]
490        script_sig: ScriptSig,
491        /// The script sequence number.
492        sequence: u32,
493        /// The value of the output being spent in ZEC.
494        #[serde(skip_serializing_if = "Option::is_none")]
495        value: Option<f64>,
496        /// The value of the output being spent, in zats, named to match zcashd.
497        #[serde(rename = "valueSat", skip_serializing_if = "Option::is_none")]
498        value_zat: Option<i64>,
499        /// The address of the output being spent.
500        #[serde(skip_serializing_if = "Option::is_none")]
501        address: Option<String>,
502        /// The output being spent, only present at `getblock` verbosity 3.
503        ///
504        /// Boxed to keep the common (verbosity 1/2) input small.
505        #[serde(skip_serializing_if = "Option::is_none")]
506        prevout: Option<Box<Prevout>>,
507    },
508}
509
510/// The previous output spent by a transparent input.
511///
512/// Only present at `getblock` verbosity 3. The fields match Bitcoin Core's `getblock`
513/// verbosity 3 `prevout` object.
514#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
515pub struct Prevout {
516    /// Whether the output being spent was created by a coinbase transaction.
517    #[getter(copy)]
518    generated: bool,
519    /// The height of the block that created the output being spent.
520    #[getter(copy)]
521    height: u32,
522    /// The value of the output being spent in ZEC.
523    #[getter(copy)]
524    value: f64,
525    /// The scriptPubKey of the output being spent.
526    #[serde(rename = "scriptPubKey")]
527    script_pub_key: ScriptPubKey,
528}
529
530/// The transparent output of a transaction.
531#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
532pub struct Output {
533    /// The value in ZEC.
534    value: f64,
535    /// The value in zats.
536    #[serde(rename = "valueZat")]
537    value_zat: i64,
538    /// index.
539    n: u32,
540    /// The scriptPubKey.
541    #[serde(rename = "scriptPubKey")]
542    script_pub_key: ScriptPubKey,
543}
544
545/// The output object returned by `gettxout` RPC requests.
546#[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/// The scriptPubKey of a transaction output.
578#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
579pub struct ScriptPubKey {
580    /// the asm.
581    asm: String,
582    /// the hex.
583    #[serde(with = "hex")]
584    hex: Script,
585    /// The required sigs.
586    #[serde(rename = "reqSigs")]
587    #[serde(default)]
588    #[serde(skip_serializing_if = "Option::is_none")]
589    #[getter(copy)]
590    req_sigs: Option<u32>,
591    /// The type, eg 'pubkeyhash'.
592    r#type: String,
593    /// The addresses.
594    #[serde(default)]
595    #[serde(skip_serializing_if = "Option::is_none")]
596    addresses: Option<Vec<String>>,
597}
598
599impl ScriptPubKey {
600    /// Builds the `scriptPubKey` object for a transparent output's lock script.
601    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            // https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L271
608            // https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L45
609            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/// The scriptSig of a transaction input.
632#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
633pub struct ScriptSig {
634    /// The asm.
635    asm: String,
636    /// The hex.
637    hex: Script,
638}
639
640/// A Sprout JoinSplit of a transaction.
641#[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    /// Public input value in ZEC.
646    #[serde(rename = "vpub_old")]
647    old_public_value: f64,
648    /// Public input value in zatoshis.
649    #[serde(rename = "vpub_oldZat")]
650    old_public_value_zat: i64,
651    /// Public input value in ZEC.
652    #[serde(rename = "vpub_new")]
653    new_public_value: f64,
654    /// Public input value in zatoshis.
655    #[serde(rename = "vpub_newZat")]
656    new_public_value_zat: i64,
657    /// Merkle root of the Sprout note commitment tree.
658    #[serde(with = "hex")]
659    #[getter(copy)]
660    anchor: [u8; 32],
661    /// The nullifier of the input notes.
662    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
663    nullifiers: Vec<[u8; 32]>,
664    /// The commitments of the output notes.
665    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
666    commitments: Vec<[u8; 32]>,
667    /// The onetime public key used to encrypt the ciphertexts
668    #[serde(rename = "onetimePubKey")]
669    #[serde(with = "hex")]
670    #[getter(copy)]
671    one_time_pubkey: [u8; 32],
672    /// The random seed
673    #[serde(rename = "randomSeed")]
674    #[serde(with = "hex")]
675    #[getter(copy)]
676    random_seed: [u8; 32],
677    /// The input notes MACs.
678    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
679    macs: Vec<[u8; 32]>,
680    /// A zero-knowledge proof using the Sprout circuit.
681    #[serde(with = "hex")]
682    proof: Vec<u8>,
683    /// The output notes ciphertexts.
684    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
685    ciphertexts: Vec<Vec<u8>>,
686}
687
688/// A Sapling spend of a transaction.
689#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
690pub struct ShieldedSpend {
691    /// Value commitment to the input note.
692    #[serde(with = "hex")]
693    #[getter(skip)]
694    cv: ValueCommitment,
695    /// Merkle root of the Sapling note commitment tree.
696    #[serde(with = "hex")]
697    #[getter(copy)]
698    anchor: [u8; 32],
699    /// The nullifier of the input note.
700    #[serde(with = "hex")]
701    #[getter(copy)]
702    nullifier: [u8; 32],
703    /// The randomized public key for spendAuthSig.
704    #[serde(with = "hex")]
705    #[getter(copy)]
706    rk: [u8; 32],
707    /// A zero-knowledge proof using the Sapling Spend circuit.
708    #[serde(with = "hex")]
709    #[getter(copy)]
710    proof: [u8; 192],
711    /// A signature authorizing this Spend.
712    #[serde(rename = "spendAuthSig", with = "hex")]
713    #[getter(copy)]
714    spend_auth_sig: [u8; 64],
715}
716
717// We can't use `#[getter(copy)]` as upstream `sapling_crypto::note::ValueCommitment` is not `Copy`.
718impl ShieldedSpend {
719    /// The value commitment to the input note.
720    pub fn cv(&self) -> ValueCommitment {
721        self.cv.clone()
722    }
723}
724
725/// A Sapling output of a transaction.
726#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
727pub struct ShieldedOutput {
728    /// Value commitment to the input note.
729    #[serde(with = "hex")]
730    #[getter(skip)]
731    cv: ValueCommitment,
732    /// The u-coordinate of the note commitment for the output note.
733    #[serde(rename = "cmu", with = "hex")]
734    cm_u: [u8; 32],
735    /// A Jubjub public key.
736    #[serde(rename = "ephemeralKey", with = "hex")]
737    ephemeral_key: [u8; 32],
738    /// The output note encrypted to the recipient.
739    #[serde(rename = "encCiphertext", with = "arrayhex")]
740    enc_ciphertext: [u8; 580],
741    /// A ciphertext enabling the sender to recover the output note.
742    #[serde(rename = "outCiphertext", with = "hex")]
743    out_ciphertext: [u8; 80],
744    /// A zero-knowledge proof using the Sapling Output circuit.
745    #[serde(with = "hex")]
746    proof: [u8; 192],
747}
748
749// We can't use `#[getter(copy)]` as upstream `sapling_crypto::note::ValueCommitment` is not `Copy`.
750impl ShieldedOutput {
751    /// The value commitment to the output note.
752    pub fn cv(&self) -> ValueCommitment {
753        self.cv.clone()
754    }
755}
756
757/// Object with Orchard-specific information.
758#[serde_with::serde_as]
759#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
760pub struct Orchard {
761    /// Array of Orchard actions.
762    actions: Vec<OrchardAction>,
763    /// The net value of Orchard Actions in ZEC.
764    #[serde(rename = "valueBalance")]
765    value_balance: f64,
766    /// The net value of Orchard Actions in zatoshis.
767    #[serde(rename = "valueBalanceZat")]
768    value_balance_zat: i64,
769    /// The flags.
770    #[serde(skip_serializing_if = "Option::is_none")]
771    flags: Option<OrchardFlags>,
772    /// A root of the Orchard note commitment tree at some block height in the past
773    #[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    /// Encoding of aggregated zk-SNARK proofs for Orchard Actions
778    #[serde_as(as = "Option<serde_with::hex::Hex>")]
779    #[serde(skip_serializing_if = "Option::is_none")]
780    proof: Option<Vec<u8>>,
781    /// An Orchard binding signature on the SIGHASH transaction hash
782    #[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/// Object with Orchard-specific information.
790#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
791pub struct OrchardFlags {
792    /// Whether Orchard outputs are enabled.
793    #[serde(rename = "enableOutputs")]
794    enable_outputs: bool,
795    /// Whether Orchard spends are enabled.
796    #[serde(rename = "enableSpends")]
797    enable_spends: bool,
798}
799
800/// The Orchard action of a transaction.
801#[allow(clippy::too_many_arguments)]
802#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
803pub struct OrchardAction {
804    /// A value commitment to the net value of the input note minus the output note.
805    #[serde(with = "hex")]
806    cv: [u8; 32],
807    /// The nullifier of the input note.
808    #[serde(with = "hex")]
809    nullifier: [u8; 32],
810    /// The randomized validating key for spendAuthSig.
811    #[serde(with = "hex")]
812    rk: [u8; 32],
813    /// The x-coordinate of the note commitment for the output note.
814    #[serde(rename = "cmx", with = "hex")]
815    cm_x: [u8; 32],
816    /// An encoding of an ephemeral Pallas public key.
817    #[serde(rename = "ephemeralKey", with = "hex")]
818    ephemeral_key: [u8; 32],
819    /// The output note encrypted to the recipient.
820    #[serde(rename = "encCiphertext", with = "arrayhex")]
821    enc_ciphertext: [u8; 580],
822    /// A ciphertext enabling the sender to recover the output note.
823    #[serde(rename = "spendAuthSig", with = "hex")]
824    spend_auth_sig: [u8; 64],
825    /// A signature authorizing the spend in this Action.
826    #[serde(rename = "outCiphertext", with = "hex")]
827    out_ciphertext: [u8; 80],
828}
829
830/// Builds the RPC object for an Orchard-shaped shielded pool (Orchard or Ironwood) from its
831/// shielded data and net value balance.
832///
833/// The Ironwood pool reuses the Orchard bundle shape, so both pools serialize through the same
834/// [`Orchard`] object; the caller selects which pool's shielded data and value balance to pass.
835/// Builds the RPC object for an Orchard-shaped bundle.
836///
837/// Both the Orchard and Ironwood pools use the same bundle type and the same wire shape, so this
838/// serves both. The caller supplies the pool's own value balance, since that is read from a
839/// separate field per pool.
840fn 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            // Display order is reversed in the RPC output.
870            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    /// Converts `tx` and `height` into a new `GetRawTransaction` in the `verbose` format.
918    #[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                // Side chain
937                Some(-1)
938            } else {
939                // Mempool
940                None
941            },
942            confirmations: if in_active_chain.unwrap_or_default() {
943                confirmations
944            } else if block_hash.is_some() {
945                // Side chain
946                Some(0)
947            } else {
948                // Mempool
949                None
950            },
951            // Only populated at `getblock` verbosity 3.
952            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                            // https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L240
972                            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                    // `JsDescription` stores every field in wire order; the RPC renders the
1043                    // 32-byte fields in display (reversed) order, matching zcashd.
1044                    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                // Display order is reversed in the RPC output.
1091                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            // zcashd includes expiryheight only for Overwinter+ transactions.
1106            // For those, expiry_height of 0 means "no expiry" per ZIP-203.
1107            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    /// Fills in each transparent input's `prevout` and the transaction `fee` from the outputs
1118    /// `tx` spends, for `getblock` verbosity 3.
1119    ///
1120    /// `spent_utxos` maps each spent outpoint to the output it spends. Coinbase transactions
1121    /// spend no outputs and have no fee, so they are left unchanged. A `prevout` object is set
1122    /// for every input whose spent output is present in `spent_utxos`; the `fee` is only set when
1123    /// every spent output is present, because the value balance is otherwise incomplete.
1124    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        // `self.inputs` is built one-to-one from `tx.inputs()`, so pair each rendered input with
1135        // its own source input and use that input's outpoint. This keeps the alignment correct
1136        // even if an input carries no outpoint, rather than relying on the filtered
1137        // `spent_outpoints()` iterator staying in step.
1138        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        // The fee needs every spent output to complete the value balance.
1154        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    /// Returns the first non-coinbase, transparent-only transaction in the mainnet block vectors.
1174    ///
1175    /// "Transparent-only" means it has transparent inputs and no shielded data, so its fee is just
1176    /// the difference between the transparent input and output totals.
1177    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    /// `add_prevouts` fills every input's `prevout` and computes the `fee` when all spent outputs
1199    /// are present.
1200    #[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        // Put the whole input value on the first input, so the fee is exactly `FEE`.
1214        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        // Every transparent input has a prevout.
1245        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        // The first input's prevout carries the fabricated fields.
1253        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        // For a transparent-only transaction the fee is inputs minus outputs.
1262        let expected_fee = Zec::from(Amount::<NonNegative>::try_from(FEE).unwrap()).lossy_zec();
1263        assert_eq!(object.fee(), Some(expected_fee));
1264    }
1265
1266    /// `add_prevouts` leaves a coinbase transaction unchanged: no `fee`, no `prevout`.
1267    #[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    /// When a spent output is missing, `add_prevouts` sets no `prevout` for it and omits the `fee`,
1293    /// because the value balance would be incomplete.
1294    #[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        // No spent outputs supplied.
1310        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    /// `vjoinsplit` must be populated for transactions with Sprout JoinSplits.
1325    ///
1326    /// The existing `getrawtransaction` snapshots all use blocks that predate Sprout, so an empty
1327    /// `vjoinsplit` is byte-identical to a correct one there and cannot catch a regression. This
1328    /// uses mainnet block 419,201, which contains real JoinSplits.
1329    #[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        // Field-level check against the parsed transaction, in the RPC's display byte order.
1363        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        // These two fields have no upstream accessor and are read back out of the wire encoding.
1393        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        // V4 JoinSplits carry Groth16 proofs.
1401        assert_eq!(rendered.proof.len(), 192);
1402    }
1403
1404    /// The `proof` field must carry the 296-byte PHGR13 proof for V2/V3 transactions.
1405    ///
1406    /// The sibling test above only covers V4 (Groth16) JoinSplits. PHGR13 proofs have no
1407    /// upstream accessor, so a Groth16-only extraction silently renders them as empty; this
1408    /// uses testnet block 141,042, whose non-coinbase transactions are V2 with one JoinSplit
1409    /// each.
1410    #[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        // The proof variant is what selects the extraction path, so assert on it directly rather
1431        // than on the transaction version that implies it.
1432        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        // The length alone would hold even if the proof were read from the wrong offset, because
1458        // the extracted slice is always `PHGR_PROOF_SIZE` bytes wide. Pin the bytes instead: the
1459        // proof must appear verbatim in the transaction's wire encoding, immediately after
1460        // `vmacs[1]` and immediately before the first ciphertext.
1461        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    /// The `Default` impl and coinbase path legitimately have no JoinSplits.
1482    #[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}