Skip to main content

zebra_chain/transaction/
compat.rs

1//! Conversions between Zebra and `zcash_primitives` transaction types.
2
3#[cfg(any(test, feature = "proptest-impl"))]
4use zcash_protocol::value::Zatoshis;
5
6use zcash_primitives::transaction::{self as zp_tx};
7
8use crate::{
9    amount::Amount,
10    block,
11    serialization::SerializationError,
12    transaction::{self, LockTime},
13    transparent::{self, OutPoint, Script},
14};
15
16// ── Transparent Input ────────────────────────────────────────────────
17
18/// Convert a librustzcash `TxIn<Authorized>` into a Zebra `transparent::Input`.
19///
20/// Coinbase inputs are detected by checking for the null `OutPoint`.
21pub fn txin_to_input(
22    txin: &zcash_transparent::bundle::TxIn<zcash_transparent::bundle::Authorized>,
23) -> Result<transparent::Input, SerializationError> {
24    if *txin.prevout() == zcash_transparent::bundle::OutPoint::NULL {
25        // Coinbase input: the script_sig contains the encoded height + miner data.
26        //
27        // The genesis coinbase predates BIP-34, so its script has no height prefix and must be
28        // special-cased, exactly as `Input::zcash_deserialize` does. Parsing it as a height push
29        // would otherwise yield a bogus height instead of `Height::MIN`.
30        let script_bytes = txin.script_sig().0 .0.clone();
31        let (height, data) = if script_bytes.as_slice()
32            == crate::transparent::serialize::GENESIS_COINBASE_SCRIPT_SIG
33        {
34            (
35                block::Height::MIN,
36                crate::transparent::serialize::GENESIS_COINBASE_SCRIPT_SIG.to_vec(),
37            )
38        } else {
39            crate::transparent::serialize::parse_coinbase_height(&script_bytes)?
40        };
41        Ok(transparent::Input::Coinbase {
42            height,
43            data,
44            sequence: txin.sequence(),
45        })
46    } else {
47        let prevout = txin.prevout();
48        let hash_bytes: [u8; 32] = *prevout.hash();
49        Ok(transparent::Input::PrevOut {
50            outpoint: OutPoint {
51                hash: transaction::Hash(hash_bytes),
52                index: prevout.n(),
53            },
54            unlock_script: Script::new(&txin.script_sig().0 .0),
55            sequence: txin.sequence(),
56        })
57    }
58}
59
60/// Convert a Zebra `transparent::Input` into a librustzcash `TxIn<Authorized>`.
61#[cfg(any(test, feature = "proptest-impl"))]
62pub fn input_to_txin(
63    input: &transparent::Input,
64) -> zcash_transparent::bundle::TxIn<zcash_transparent::bundle::Authorized> {
65    match input {
66        transparent::Input::PrevOut {
67            outpoint,
68            unlock_script,
69            sequence,
70        } => {
71            let zp_outpoint =
72                zcash_transparent::bundle::OutPoint::new(outpoint.hash.0, outpoint.index);
73            zcash_transparent::bundle::TxIn::from_parts(
74                zp_outpoint,
75                zcash_transparent::address::Script(zcash_script::script::Code(
76                    unlock_script.as_raw_bytes().to_vec(),
77                )),
78                *sequence,
79            )
80        }
81        transparent::Input::Coinbase { sequence, .. } => {
82            // Reconstruct the full script_sig (encoded height followed by the miner data),
83            // matching the wire format. `coinbase_script` handles the genesis special case, and
84            // only returns `None` for a hand-built genesis-height input whose data is not the
85            // genesis coinbase, which cannot come from a deserialized transaction.
86            let script_bytes = input
87                .coinbase_script()
88                .expect("coinbase_script reconstructs from a deserialized coinbase input");
89
90            zcash_transparent::bundle::TxIn::from_parts(
91                zcash_transparent::bundle::OutPoint::NULL,
92                zcash_transparent::address::Script(zcash_script::script::Code(script_bytes)),
93                *sequence,
94            )
95        }
96    }
97}
98
99// ── Transparent Output ───────────────────────────────────────────────
100
101/// Convert a librustzcash `TxOut` into a Zebra `transparent::Output`.
102pub fn txout_to_output(txout: &zcash_transparent::bundle::TxOut) -> transparent::Output {
103    let value_u64: u64 = txout.value().into();
104    transparent::Output {
105        value: Amount::try_from(value_u64 as i64)
106            .expect("librustzcash Zatoshis is always a valid non-negative Amount"),
107        lock_script: Script::new(&txout.script_pubkey().0 .0),
108    }
109}
110
111/// Convert a Zebra `transparent::Output` into a librustzcash `TxOut`.
112#[cfg(any(test, feature = "proptest-impl"))]
113pub fn output_to_txout(output: &transparent::Output) -> zcash_transparent::bundle::TxOut {
114    let zatoshis = Zatoshis::from_nonnegative_i64(output.value.into())
115        .expect("Zebra Amount<NonNegative> is always a valid Zatoshis");
116    zcash_transparent::bundle::TxOut::new(
117        zatoshis,
118        zcash_transparent::address::Script(zcash_script::script::Code(
119            output.lock_script.as_raw_bytes().to_vec(),
120        )),
121    )
122}
123
124// ── LockTime ─────────────────────────────────────────────────────────
125
126/// Convert a librustzcash `u32` lock_time into a Zebra `LockTime`.
127///
128/// Values below 500_000_000 are interpreted as block heights, values at or above
129/// as Unix timestamps.
130pub fn u32_to_lock_time(lock_time: u32) -> LockTime {
131    // Reuse the deserialization logic which implements the same threshold check.
132    use crate::serialization::ZcashDeserialize;
133    let bytes = lock_time.to_le_bytes();
134    LockTime::zcash_deserialize(&bytes[..]).expect("all u32 values are valid LockTimes")
135}
136
137/// Convert a Zebra `LockTime` into a `u32` for librustzcash.
138#[cfg(any(test, feature = "proptest-impl"))]
139pub fn lock_time_to_u32(lock_time: &LockTime) -> u32 {
140    use crate::serialization::ZcashSerialize;
141    let mut buf = Vec::with_capacity(4);
142    lock_time
143        .zcash_serialize(&mut buf)
144        .expect("serializing LockTime to vec should not fail");
145    u32::from_le_bytes(
146        buf.try_into()
147            .expect("LockTime serializes to exactly 4 bytes"),
148    )
149}
150
151// ── Height ───────────────────────────────────────────────────────────
152
153/// Convert a Zebra `block::Height` into a librustzcash `BlockHeight`.
154#[cfg(any(test, feature = "proptest-impl"))]
155pub fn height_to_block_height(h: block::Height) -> zcash_protocol::consensus::BlockHeight {
156    h.into()
157}
158
159/// Returns an error if the height is out of the valid Zebra range.
160#[cfg(any(test, feature = "proptest-impl", feature = "elasticsearch"))]
161pub fn block_height_to_height(
162    bh: zcash_protocol::consensus::BlockHeight,
163) -> Result<block::Height, SerializationError> {
164    block::Height::try_from(bh)
165        .map_err(|_| SerializationError::Parse("block height out of valid range"))
166}
167
168// ── NetworkUpgrade / BranchId ────────────────────────────────────────
169
170/// Convert a librustzcash `BranchId` into a Zebra `NetworkUpgrade`.
171///
172/// Returns `None` for unknown branch IDs (e.g., `Sprout` which has branch ID 0).
173pub(crate) fn branch_id_to_network_upgrade(
174    branch_id: zcash_protocol::consensus::BranchId,
175) -> Option<crate::parameters::NetworkUpgrade> {
176    crate::parameters::NetworkUpgrade::try_from(u32::from(branch_id)).ok()
177}
178
179// ── Sprout JoinSplit fields not exposed by `zcash_primitives` ─────────
180
181/// The number of note ciphertexts in a Sprout JoinSplit description.
182const ZC_NUM_JS_OUTPUTS: usize = 2;
183
184/// The size of one Sprout note ciphertext.
185pub const SPROUT_CIPHERTEXT_SIZE: usize = 601;
186
187/// The size of a Groth16 Sprout proof (v4 JoinSplits).
188const GROTH_PROOF_SIZE: usize = 48 + 96 + 48;
189
190/// The size of a PHGR13 Sprout proof (v2 and v3 JoinSplits).
191const PHGR_PROOF_SIZE: usize = 33 + 33 + 65 + 33 + 33 + 33 + 33 + 33;
192
193/// The offset of `ephemeralKey` within a serialized JoinSplit description:
194/// `vpub_old` and `vpub_new` (8 bytes each), `anchor`, two `nullifiers`, two `commitments`.
195const JS_EPHEMERAL_KEY_OFFSET: usize = 8 + 8 + 32 + (2 * 32) + (2 * 32);
196
197/// Returns the `ephemeralKey`, proof, and `encCiphertexts` of a Sprout JoinSplit description.
198///
199/// [`JsDescription`](zcash_primitives::transaction::components::sprout::JsDescription) exposes
200/// accessors for its other fields, but keeps `ephemeralKey` and `encCiphertexts` private, and only
201/// exposes the proof for the Groth16 variant, so all three are read back out of its serialization.
202/// If upstream gains accessors for them, this can be deleted.
203///
204/// The proof is 192 bytes for Groth16 (V4 transactions) and 296 bytes for PHGR13 (V2/V3
205/// transactions).
206///
207/// The returned 32-byte values are in wire order; callers that render them for RPCs must reverse
208/// them, as they do for every other 32-byte JoinSplit field. The proof and the ciphertexts are not
209/// reversed by `zcashd`, so they are rendered in the wire order returned here.
210pub fn sprout_joinsplit_key_proof_and_ciphertexts(
211    joinsplit: &zcash_primitives::transaction::components::sprout::JsDescription,
212) -> (
213    [u8; 32],
214    Vec<u8>,
215    [[u8; SPROUT_CIPHERTEXT_SIZE]; ZC_NUM_JS_OUTPUTS],
216) {
217    // `ephemeralKey`, `randomSeed`, then two `vmacs`, then the proof, then the ciphertexts.
218    let proof_offset = JS_EPHEMERAL_KEY_OFFSET + 32 + 32 + (2 * 32);
219    let proof_size = if joinsplit.groth_proof_bytes().is_some() {
220        GROTH_PROOF_SIZE
221    } else {
222        PHGR_PROOF_SIZE
223    };
224    let ciphertexts_offset = proof_offset + proof_size;
225    let joinsplit_size = ciphertexts_offset + (ZC_NUM_JS_OUTPUTS * SPROUT_CIPHERTEXT_SIZE);
226
227    let mut bytes = Vec::with_capacity(joinsplit_size);
228    joinsplit
229        .write(&mut bytes)
230        .expect("writing a JoinSplit to a vec cannot fail");
231
232    // Guard the offsets against an upstream layout change. The total length pins the size of every
233    // field, and `vmacs[1]` is the last field before the proof that is reachable through a public
234    // accessor, so finding it immediately before `proof_offset` pins the offsets read below.
235    //
236    // Checking the length first keeps the slicing below in range if upstream ever shortens the
237    // encoding.
238    debug_assert_eq!(
239        bytes.len(),
240        joinsplit_size,
241        "the JoinSplit wire layout must match the offsets used here",
242    );
243    debug_assert_eq!(
244        &bytes[proof_offset - 32..proof_offset],
245        &joinsplit.macs()[1][..],
246        "the JoinSplit wire layout must match the offsets used here",
247    );
248
249    let mut ephemeral_key = [0u8; 32];
250    ephemeral_key.copy_from_slice(&bytes[JS_EPHEMERAL_KEY_OFFSET..JS_EPHEMERAL_KEY_OFFSET + 32]);
251
252    let proof = bytes[proof_offset..ciphertexts_offset].to_vec();
253
254    let mut ciphertexts = [[0u8; SPROUT_CIPHERTEXT_SIZE]; ZC_NUM_JS_OUTPUTS];
255    for (i, ciphertext) in ciphertexts.iter_mut().enumerate() {
256        let start = ciphertexts_offset + i * SPROUT_CIPHERTEXT_SIZE;
257        ciphertext.copy_from_slice(&bytes[start..start + SPROUT_CIPHERTEXT_SIZE]);
258    }
259
260    (ephemeral_key, proof, ciphertexts)
261}
262
263// ── Rebuilding transaction data ──────────────────────────────────────
264
265/// The transparent bundle of an authorized transaction.
266type TransparentBundle = zcash_transparent::bundle::Bundle<zcash_transparent::bundle::Authorized>;
267
268/// The Sapling bundle of an authorized transaction.
269type SaplingBundle =
270    sapling_crypto::Bundle<sapling_crypto::bundle::Authorized, zcash_protocol::value::ZatBalance>;
271
272/// An Orchard-shaped bundle of an authorized transaction, used by both the Orchard and
273/// Ironwood pools.
274type OrchardBundle =
275    orchard::Bundle<orchard::bundle::Authorized, zcash_protocol::value::ZatBalance>;
276
277/// Builds [`TransactionData`] from its parts, routing v6 transactions through
278/// [`TransactionData::from_parts_v6`].
279///
280/// Use this instead of [`TransactionData::from_parts`] whenever an existing transaction is being
281/// rebuilt. `from_parts` hard-codes `ironwood_bundle: None`, so rebuilding a v6 transaction
282/// through it silently drops the Ironwood bundle. That changes the transaction ID, and if the
283/// result reaches the verifier it also skips the Ironwood proof check, because the verifier only
284/// queues that check when the bundle is present.
285///
286/// Taking `ironwood_bundle` as a required argument is the point: the compiler will not let a
287/// caller forget to carry it across. This is the only place the version dispatch is made.
288///
289/// [`TransactionData`]: zcash_primitives::transaction::TransactionData
290/// [`TransactionData::from_parts`]: zcash_primitives::transaction::TransactionData::from_parts
291/// [`TransactionData::from_parts_v6`]: zcash_primitives::transaction::TransactionData::from_parts_v6
292#[allow(clippy::too_many_arguments)]
293pub(crate) fn transaction_data_from_parts(
294    version: zp_tx::TxVersion,
295    branch_id: zcash_protocol::consensus::BranchId,
296    lock_time: u32,
297    expiry_height: zcash_protocol::consensus::BlockHeight,
298    transparent_bundle: Option<TransparentBundle>,
299    sprout_bundle: Option<zp_tx::components::sprout::Bundle>,
300    sapling_bundle: Option<SaplingBundle>,
301    orchard_bundle: Option<OrchardBundle>,
302    ironwood_bundle: Option<OrchardBundle>,
303) -> zp_tx::TransactionData<zp_tx::Authorized> {
304    if version == zp_tx::TxVersion::V6 {
305        // v6 has no Sprout component, so `sprout_bundle` is intentionally dropped here.
306        zp_tx::TransactionData::from_parts_v6(
307            branch_id,
308            lock_time,
309            expiry_height,
310            transparent_bundle,
311            sapling_bundle,
312            orchard_bundle,
313            ironwood_bundle,
314        )
315    } else {
316        debug_assert!(
317            ironwood_bundle.is_none(),
318            "only v6 transactions can carry an Ironwood bundle",
319        );
320
321        zp_tx::TransactionData::from_parts(
322            version,
323            branch_id,
324            lock_time,
325            expiry_height,
326            transparent_bundle,
327            sprout_bundle,
328            sapling_bundle,
329            orchard_bundle,
330        )
331    }
332}