zcash_primitives 0.10.0

Rust implementations of the Zcash primitives
Documentation
//! Structs and methods for handling Zcash transactions.
pub mod builder;
pub mod components;
pub mod fees;
pub mod sighash;
pub mod sighash_v4;
pub mod sighash_v5;
pub mod txid;
pub mod util;

#[cfg(test)]
mod tests;

use blake2b_simd::Hash as Blake2bHash;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use ff::PrimeField;
use memuse::DynamicUsage;
use std::convert::TryFrom;
use std::fmt;
use std::fmt::Debug;
use std::io::{self, Read, Write};
use std::ops::Deref;
use zcash_encoding::{Array, CompactSize, Vector};

use crate::{
    consensus::{BlockHeight, BranchId},
    sapling::redjubjub,
};

use self::{
    components::{
        amount::{Amount, BalanceError},
        orchard as orchard_serialization,
        sapling::{
            self, OutputDescription, OutputDescriptionV5, SpendDescription, SpendDescriptionV5,
        },
        sprout::{self, JsDescription},
        transparent::{self, TxIn, TxOut},
        OutPoint,
    },
    txid::{to_txid, BlockTxCommitmentDigester, TxIdDigester},
    util::sha256d::{HashReader, HashWriter},
};

#[cfg(feature = "zfuture")]
use self::components::tze::{self, TzeIn, TzeOut};

const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270;
const OVERWINTER_TX_VERSION: u32 = 3;
const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085;
const SAPLING_TX_VERSION: u32 = 4;

const V5_TX_VERSION: u32 = 5;
const V5_VERSION_GROUP_ID: u32 = 0x26A7270A;

/// These versions are used exclusively for in-development transaction
/// serialization, and will never be active under the consensus rules.
/// When new consensus transaction versions are added, all call sites
/// using these constants should be inspected, and use of these constants
/// should be removed as appropriate in favor of the new consensus
/// transaction version and group.
#[cfg(feature = "zfuture")]
const ZFUTURE_VERSION_GROUP_ID: u32 = 0xFFFFFFFF;
#[cfg(feature = "zfuture")]
const ZFUTURE_TX_VERSION: u32 = 0x0000FFFF;

#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct TxId([u8; 32]);

memuse::impl_no_dynamic_usage!(TxId);

impl fmt::Debug for TxId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // The (byte-flipped) hex string is more useful than the raw bytes, because we can
        // look that up in RPC methods and block explorers.
        let txid_str = self.to_string();
        f.debug_tuple("TxId").field(&txid_str).finish()
    }
}

impl fmt::Display for TxId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut data = self.0;
        data.reverse();
        formatter.write_str(&hex::encode(data))
    }
}

impl AsRef<[u8; 32]> for TxId {
    fn as_ref(&self) -> &[u8; 32] {
        &self.0
    }
}

impl TxId {
    pub fn from_bytes(bytes: [u8; 32]) -> Self {
        TxId(bytes)
    }

    pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
        let mut hash = [0u8; 32];
        reader.read_exact(&mut hash)?;
        Ok(TxId::from_bytes(hash))
    }

    pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
        writer.write_all(&self.0)?;
        Ok(())
    }
}

/// The set of defined transaction format versions.
///
/// This is serialized in the first four or eight bytes of the transaction format, and
/// represents valid combinations of the `(overwintered, version, version_group_id)`
/// transaction fields. Note that this is not dependent on epoch, only on transaction encoding.
/// For example, if a particular epoch defines a new transaction version but also allows the
/// previous version, then only the new version would be added to this enum.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TxVersion {
    Sprout(u32),
    Overwinter,
    Sapling,
    Zip225,
    #[cfg(feature = "zfuture")]
    ZFuture,
}

impl TxVersion {
    pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
        let header = reader.read_u32::<LittleEndian>()?;
        let overwintered = (header >> 31) == 1;
        let version = header & 0x7FFFFFFF;

        if overwintered {
            match (version, reader.read_u32::<LittleEndian>()?) {
                (OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID) => Ok(TxVersion::Overwinter),
                (SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID) => Ok(TxVersion::Sapling),
                (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225),
                #[cfg(feature = "zfuture")]
                (ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID) => Ok(TxVersion::ZFuture),
                _ => Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "Unknown transaction format",
                )),
            }
        } else if version >= 1 {
            Ok(TxVersion::Sprout(version))
        } else {
            Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Unknown transaction format",
            ))
        }
    }

    pub fn header(&self) -> u32 {
        // After Sprout, the overwintered bit is always set.
        let overwintered = match self {
            TxVersion::Sprout(_) => 0,
            _ => 1 << 31,
        };

        overwintered
            | match self {
                TxVersion::Sprout(v) => *v,
                TxVersion::Overwinter => OVERWINTER_TX_VERSION,
                TxVersion::Sapling => SAPLING_TX_VERSION,
                TxVersion::Zip225 => V5_TX_VERSION,
                #[cfg(feature = "zfuture")]
                TxVersion::ZFuture => ZFUTURE_TX_VERSION,
            }
    }

    pub fn version_group_id(&self) -> u32 {
        match self {
            TxVersion::Sprout(_) => 0,
            TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID,
            TxVersion::Sapling => SAPLING_VERSION_GROUP_ID,
            TxVersion::Zip225 => V5_VERSION_GROUP_ID,
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID,
        }
    }

    pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
        writer.write_u32::<LittleEndian>(self.header())?;
        match self {
            TxVersion::Sprout(_) => Ok(()),
            _ => writer.write_u32::<LittleEndian>(self.version_group_id()),
        }
    }

    pub fn has_sprout(&self) -> bool {
        match self {
            TxVersion::Sprout(v) => *v >= 2u32,
            TxVersion::Overwinter | TxVersion::Sapling => true,
            TxVersion::Zip225 => false,
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => true,
        }
    }

    pub fn has_overwinter(&self) -> bool {
        !matches!(self, TxVersion::Sprout(_))
    }

    pub fn has_sapling(&self) -> bool {
        match self {
            TxVersion::Sprout(_) | TxVersion::Overwinter => false,
            TxVersion::Sapling => true,
            TxVersion::Zip225 => true,
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => true,
        }
    }

    pub fn has_orchard(&self) -> bool {
        match self {
            TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => false,
            TxVersion::Zip225 => true,
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => true,
        }
    }

    #[cfg(feature = "zfuture")]
    pub fn has_tze(&self) -> bool {
        matches!(self, TxVersion::ZFuture)
    }

    pub fn suggested_for_branch(consensus_branch_id: BranchId) -> Self {
        match consensus_branch_id {
            BranchId::Sprout => TxVersion::Sprout(2),
            BranchId::Overwinter => TxVersion::Overwinter,
            BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => {
                TxVersion::Sapling
            }
            BranchId::Nu5 => TxVersion::Zip225,
            #[cfg(feature = "zfuture")]
            BranchId::ZFuture => TxVersion::ZFuture,
        }
    }
}

/// Authorization state for a bundle of transaction data.
pub trait Authorization {
    type TransparentAuth: transparent::Authorization;
    type SaplingAuth: sapling::Authorization;
    type OrchardAuth: orchard::bundle::Authorization;

    #[cfg(feature = "zfuture")]
    type TzeAuth: tze::Authorization;
}

#[derive(Debug)]
pub struct Authorized;

impl Authorization for Authorized {
    type TransparentAuth = transparent::Authorized;
    type SaplingAuth = sapling::Authorized;
    type OrchardAuth = orchard::bundle::Authorized;

    #[cfg(feature = "zfuture")]
    type TzeAuth = tze::Authorized;
}

pub struct Unauthorized;

impl Authorization for Unauthorized {
    type TransparentAuth = transparent::builder::Unauthorized;
    type SaplingAuth = sapling::builder::Unauthorized;
    type OrchardAuth = orchard_serialization::Unauthorized;

    #[cfg(feature = "zfuture")]
    type TzeAuth = tze::builder::Unauthorized;
}

/// A Zcash transaction.
#[derive(Debug)]
pub struct Transaction {
    txid: TxId,
    data: TransactionData<Authorized>,
}

impl Deref for Transaction {
    type Target = TransactionData<Authorized>;

    fn deref(&self) -> &TransactionData<Authorized> {
        &self.data
    }
}

impl PartialEq for Transaction {
    fn eq(&self, other: &Transaction) -> bool {
        self.txid == other.txid
    }
}

#[derive(Debug)]
pub struct TransactionData<A: Authorization> {
    version: TxVersion,
    consensus_branch_id: BranchId,
    lock_time: u32,
    expiry_height: BlockHeight,
    transparent_bundle: Option<transparent::Bundle<A::TransparentAuth>>,
    sprout_bundle: Option<sprout::Bundle>,
    sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
    orchard_bundle: Option<orchard::bundle::Bundle<A::OrchardAuth, Amount>>,
    #[cfg(feature = "zfuture")]
    tze_bundle: Option<tze::Bundle<A::TzeAuth>>,
}

impl<A: Authorization> TransactionData<A> {
    #[allow(clippy::too_many_arguments)]
    pub fn from_parts(
        version: TxVersion,
        consensus_branch_id: BranchId,
        lock_time: u32,
        expiry_height: BlockHeight,
        transparent_bundle: Option<transparent::Bundle<A::TransparentAuth>>,
        sprout_bundle: Option<sprout::Bundle>,
        sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
        orchard_bundle: Option<orchard::Bundle<A::OrchardAuth, Amount>>,
    ) -> Self {
        TransactionData {
            version,
            consensus_branch_id,
            lock_time,
            expiry_height,
            transparent_bundle,
            sprout_bundle,
            sapling_bundle,
            orchard_bundle,
            #[cfg(feature = "zfuture")]
            tze_bundle: None,
        }
    }

    #[cfg(feature = "zfuture")]
    #[allow(clippy::too_many_arguments)]
    pub fn from_parts_zfuture(
        version: TxVersion,
        consensus_branch_id: BranchId,
        lock_time: u32,
        expiry_height: BlockHeight,
        transparent_bundle: Option<transparent::Bundle<A::TransparentAuth>>,
        sprout_bundle: Option<sprout::Bundle>,
        sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
        orchard_bundle: Option<orchard::Bundle<A::OrchardAuth, Amount>>,
        tze_bundle: Option<tze::Bundle<A::TzeAuth>>,
    ) -> Self {
        TransactionData {
            version,
            consensus_branch_id,
            lock_time,
            expiry_height,
            transparent_bundle,
            sprout_bundle,
            sapling_bundle,
            orchard_bundle,
            tze_bundle,
        }
    }

    pub fn version(&self) -> TxVersion {
        self.version
    }

    pub fn consensus_branch_id(&self) -> BranchId {
        self.consensus_branch_id
    }

    pub fn lock_time(&self) -> u32 {
        self.lock_time
    }

    pub fn expiry_height(&self) -> BlockHeight {
        self.expiry_height
    }

    pub fn transparent_bundle(&self) -> Option<&transparent::Bundle<A::TransparentAuth>> {
        self.transparent_bundle.as_ref()
    }

    pub fn sprout_bundle(&self) -> Option<&sprout::Bundle> {
        self.sprout_bundle.as_ref()
    }

    pub fn sapling_bundle(&self) -> Option<&sapling::Bundle<A::SaplingAuth>> {
        self.sapling_bundle.as_ref()
    }

    pub fn orchard_bundle(&self) -> Option<&orchard::Bundle<A::OrchardAuth, Amount>> {
        self.orchard_bundle.as_ref()
    }

    #[cfg(feature = "zfuture")]
    pub fn tze_bundle(&self) -> Option<&tze::Bundle<A::TzeAuth>> {
        self.tze_bundle.as_ref()
    }

    /// Returns the total fees paid by the transaction, given a function that can be used to
    /// retrieve the value of previous transactions' transparent outputs that are being spent in
    /// this transaction.
    pub fn fee_paid<E, F>(&self, get_prevout: F) -> Result<Amount, E>
    where
        E: From<BalanceError>,
        F: FnMut(&OutPoint) -> Result<Amount, E>,
    {
        let value_balances = [
            self.transparent_bundle
                .as_ref()
                .map_or_else(|| Ok(Amount::zero()), |b| b.value_balance(get_prevout))?,
            self.sprout_bundle.as_ref().map_or_else(
                || Ok(Amount::zero()),
                |b| b.value_balance().ok_or(BalanceError::Overflow),
            )?,
            self.sapling_bundle
                .as_ref()
                .map_or_else(Amount::zero, |b| *b.value_balance()),
            self.orchard_bundle
                .as_ref()
                .map_or_else(Amount::zero, |b| *b.value_balance()),
        ];

        value_balances
            .iter()
            .sum::<Option<_>>()
            .ok_or_else(|| BalanceError::Overflow.into())
    }

    pub fn digest<D: TransactionDigest<A>>(&self, digester: D) -> D::Digest {
        digester.combine(
            digester.digest_header(
                self.version,
                self.consensus_branch_id,
                self.lock_time,
                self.expiry_height,
            ),
            digester.digest_transparent(self.transparent_bundle.as_ref()),
            digester.digest_sapling(self.sapling_bundle.as_ref()),
            digester.digest_orchard(self.orchard_bundle.as_ref()),
            #[cfg(feature = "zfuture")]
            digester.digest_tze(self.tze_bundle.as_ref()),
        )
    }

    /// Maps the bundles from one type to another.
    ///
    /// This shouldn't be necessary for most use cases; it is provided for handling the
    /// cross-FFI builder logic in `zcashd`.
    pub fn map_bundles<B: Authorization>(
        self,
        f_transparent: impl FnOnce(
            Option<transparent::Bundle<A::TransparentAuth>>,
        ) -> Option<transparent::Bundle<B::TransparentAuth>>,
        f_sapling: impl FnOnce(
            Option<sapling::Bundle<A::SaplingAuth>>,
        ) -> Option<sapling::Bundle<B::SaplingAuth>>,
        f_orchard: impl FnOnce(
            Option<orchard::bundle::Bundle<A::OrchardAuth, Amount>>,
        ) -> Option<orchard::bundle::Bundle<B::OrchardAuth, Amount>>,
        #[cfg(feature = "zfuture")] f_tze: impl FnOnce(
            Option<tze::Bundle<A::TzeAuth>>,
        ) -> Option<tze::Bundle<B::TzeAuth>>,
    ) -> TransactionData<B> {
        TransactionData {
            version: self.version,
            consensus_branch_id: self.consensus_branch_id,
            lock_time: self.lock_time,
            expiry_height: self.expiry_height,
            transparent_bundle: f_transparent(self.transparent_bundle),
            sprout_bundle: self.sprout_bundle,
            sapling_bundle: f_sapling(self.sapling_bundle),
            orchard_bundle: f_orchard(self.orchard_bundle),
            #[cfg(feature = "zfuture")]
            tze_bundle: f_tze(self.tze_bundle),
        }
    }

    pub fn map_authorization<B: Authorization>(
        self,
        f_transparent: impl transparent::MapAuth<A::TransparentAuth, B::TransparentAuth>,
        f_sapling: impl sapling::MapAuth<A::SaplingAuth, B::SaplingAuth>,
        mut f_orchard: impl orchard_serialization::MapAuth<A::OrchardAuth, B::OrchardAuth>,
        #[cfg(feature = "zfuture")] f_tze: impl tze::MapAuth<A::TzeAuth, B::TzeAuth>,
    ) -> TransactionData<B> {
        TransactionData {
            version: self.version,
            consensus_branch_id: self.consensus_branch_id,
            lock_time: self.lock_time,
            expiry_height: self.expiry_height,
            transparent_bundle: self
                .transparent_bundle
                .map(|b| b.map_authorization(f_transparent)),
            sprout_bundle: self.sprout_bundle,
            sapling_bundle: self.sapling_bundle.map(|b| b.map_authorization(f_sapling)),
            orchard_bundle: self.orchard_bundle.map(|b| {
                b.map_authorization(
                    &mut f_orchard,
                    |f, _, s| f.map_spend_auth(s),
                    |f, a| f.map_authorization(a),
                )
            }),
            #[cfg(feature = "zfuture")]
            tze_bundle: self.tze_bundle.map(|b| b.map_authorization(f_tze)),
        }
    }
}

impl<A: Authorization> TransactionData<A> {
    pub fn sapling_value_balance(&self) -> Amount {
        self.sapling_bundle
            .as_ref()
            .map_or(Amount::zero(), |b| *b.value_balance())
    }
}

impl TransactionData<Authorized> {
    pub fn freeze(self) -> io::Result<Transaction> {
        Transaction::from_data(self)
    }
}

impl Transaction {
    fn from_data(data: TransactionData<Authorized>) -> io::Result<Self> {
        match data.version {
            TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
                Self::from_data_v4(data)
            }
            TxVersion::Zip225 => Ok(Self::from_data_v5(data)),
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => Ok(Self::from_data_v5(data)),
        }
    }

    fn from_data_v4(data: TransactionData<Authorized>) -> io::Result<Self> {
        let mut tx = Transaction {
            txid: TxId([0; 32]),
            data,
        };
        let mut writer = HashWriter::default();
        tx.write(&mut writer)?;
        tx.txid.0.copy_from_slice(&writer.into_hash());
        Ok(tx)
    }

    fn from_data_v5(data: TransactionData<Authorized>) -> Self {
        let txid = to_txid(
            data.version,
            data.consensus_branch_id,
            &data.digest(TxIdDigester),
        );

        Transaction { txid, data }
    }

    pub fn into_data(self) -> TransactionData<Authorized> {
        self.data
    }

    pub fn txid(&self) -> TxId {
        self.txid
    }

    pub fn read<R: Read>(reader: R, consensus_branch_id: BranchId) -> io::Result<Self> {
        let mut reader = HashReader::new(reader);

        let version = TxVersion::read(&mut reader)?;
        match version {
            TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
                Self::read_v4(reader, version, consensus_branch_id)
            }
            TxVersion::Zip225 => Self::read_v5(reader.into_base_reader(), version),
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => Self::read_v5(reader.into_base_reader(), version),
        }
    }

    #[allow(clippy::redundant_closure)]
    fn read_v4<R: Read>(
        mut reader: HashReader<R>,
        version: TxVersion,
        consensus_branch_id: BranchId,
    ) -> io::Result<Self> {
        let transparent_bundle = Self::read_transparent(&mut reader)?;

        let lock_time = reader.read_u32::<LittleEndian>()?;
        let expiry_height: BlockHeight = if version.has_overwinter() {
            reader.read_u32::<LittleEndian>()?.into()
        } else {
            0u32.into()
        };

        let (value_balance, shielded_spends, shielded_outputs) = if version.has_sapling() {
            let vb = Self::read_amount(&mut reader)?;
            #[allow(clippy::redundant_closure)]
            let ss: Vec<SpendDescription<sapling::Authorized>> =
                Vector::read(&mut reader, |r| SpendDescription::read(r))?;
            #[allow(clippy::redundant_closure)]
            let so: Vec<OutputDescription<sapling::GrothProofBytes>> =
                Vector::read(&mut reader, |r| OutputDescription::read(r))?;
            (vb, ss, so)
        } else {
            (Amount::zero(), vec![], vec![])
        };

        let sprout_bundle = if version.has_sprout() {
            let joinsplits = Vector::read(&mut reader, |r| {
                JsDescription::read(r, version.has_sapling())
            })?;

            if !joinsplits.is_empty() {
                let mut bundle = sprout::Bundle {
                    joinsplits,
                    joinsplit_pubkey: [0; 32],
                    joinsplit_sig: [0; 64],
                };
                reader.read_exact(&mut bundle.joinsplit_pubkey)?;
                reader.read_exact(&mut bundle.joinsplit_sig)?;
                Some(bundle)
            } else {
                None
            }
        } else {
            None
        };

        let binding_sig = if version.has_sapling()
            && !(shielded_spends.is_empty() && shielded_outputs.is_empty())
        {
            Some(redjubjub::Signature::read(&mut reader)?)
        } else {
            None
        };

        let mut txid = [0; 32];
        let hash_bytes = reader.into_hash();
        txid.copy_from_slice(&hash_bytes);

        Ok(Transaction {
            txid: TxId(txid),
            data: TransactionData {
                version,
                consensus_branch_id,
                lock_time,
                expiry_height,
                transparent_bundle,
                sprout_bundle,
                sapling_bundle: binding_sig.map(|binding_sig| {
                    sapling::Bundle::from_parts(
                        shielded_spends,
                        shielded_outputs,
                        value_balance,
                        sapling::Authorized { binding_sig },
                    )
                }),
                orchard_bundle: None,
                #[cfg(feature = "zfuture")]
                tze_bundle: None,
            },
        })
    }

    fn read_transparent<R: Read>(
        mut reader: R,
    ) -> io::Result<Option<transparent::Bundle<transparent::Authorized>>> {
        let vin = Vector::read(&mut reader, TxIn::read)?;
        let vout = Vector::read(&mut reader, TxOut::read)?;
        Ok(if vin.is_empty() && vout.is_empty() {
            None
        } else {
            Some(transparent::Bundle {
                vin,
                vout,
                authorization: transparent::Authorized,
            })
        })
    }

    fn read_amount<R: Read>(mut reader: R) -> io::Result<Amount> {
        let mut tmp = [0; 8];
        reader.read_exact(&mut tmp)?;
        Amount::from_i64_le_bytes(tmp)
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range"))
    }

    fn read_v5<R: Read>(mut reader: R, version: TxVersion) -> io::Result<Self> {
        let (consensus_branch_id, lock_time, expiry_height) =
            Self::read_v5_header_fragment(&mut reader)?;
        let transparent_bundle = Self::read_transparent(&mut reader)?;
        let sapling_bundle = Self::read_v5_sapling(&mut reader)?;
        let orchard_bundle = orchard_serialization::read_v5_bundle(&mut reader)?;

        #[cfg(feature = "zfuture")]
        let tze_bundle = if version.has_tze() {
            Self::read_tze(&mut reader)?
        } else {
            None
        };

        let data = TransactionData {
            version,
            consensus_branch_id,
            lock_time,
            expiry_height,
            transparent_bundle,
            sprout_bundle: None,
            sapling_bundle,
            orchard_bundle,
            #[cfg(feature = "zfuture")]
            tze_bundle,
        };

        Ok(Self::from_data_v5(data))
    }

    fn read_v5_header_fragment<R: Read>(mut reader: R) -> io::Result<(BranchId, u32, BlockHeight)> {
        let consensus_branch_id = reader.read_u32::<LittleEndian>().and_then(|value| {
            BranchId::try_from(value).map_err(|e| {
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "invalid consensus branch id: ".to_owned() + e,
                )
            })
        })?;
        let lock_time = reader.read_u32::<LittleEndian>()?;
        let expiry_height: BlockHeight = reader.read_u32::<LittleEndian>()?.into();
        Ok((consensus_branch_id, lock_time, expiry_height))
    }

    #[allow(clippy::redundant_closure)]
    fn read_v5_sapling<R: Read>(
        mut reader: R,
    ) -> io::Result<Option<sapling::Bundle<sapling::Authorized>>> {
        let sd_v5s = Vector::read(&mut reader, SpendDescriptionV5::read)?;
        let od_v5s = Vector::read(&mut reader, OutputDescriptionV5::read)?;
        let n_spends = sd_v5s.len();
        let n_outputs = od_v5s.len();
        let value_balance = if n_spends > 0 || n_outputs > 0 {
            Self::read_amount(&mut reader)?
        } else {
            Amount::zero()
        };

        let anchor = if n_spends > 0 {
            Some(sapling::read_base(&mut reader, "anchor")?)
        } else {
            None
        };

        let v_spend_proofs = Array::read(&mut reader, n_spends, |r| sapling::read_zkproof(r))?;
        let v_spend_auth_sigs = Array::read(&mut reader, n_spends, |r| {
            SpendDescription::read_spend_auth_sig(r)
        })?;
        let v_output_proofs = Array::read(&mut reader, n_outputs, |r| sapling::read_zkproof(r))?;

        let binding_sig = if n_spends > 0 || n_outputs > 0 {
            Some(redjubjub::Signature::read(&mut reader)?)
        } else {
            None
        };

        let shielded_spends = sd_v5s
            .into_iter()
            .zip(
                v_spend_proofs
                    .into_iter()
                    .zip(v_spend_auth_sigs.into_iter()),
            )
            .map(|(sd_5, (zkproof, spend_auth_sig))| {
                // the following `unwrap` is safe because we know n_spends > 0.
                sd_5.into_spend_description(anchor.unwrap(), zkproof, spend_auth_sig)
            })
            .collect();

        let shielded_outputs = od_v5s
            .into_iter()
            .zip(v_output_proofs.into_iter())
            .map(|(od_5, zkproof)| od_5.into_output_description(zkproof))
            .collect();

        Ok(binding_sig.map(|binding_sig| {
            sapling::Bundle::from_parts(
                shielded_spends,
                shielded_outputs,
                value_balance,
                sapling::Authorized { binding_sig },
            )
        }))
    }

    #[cfg(feature = "zfuture")]
    fn read_tze<R: Read>(mut reader: &mut R) -> io::Result<Option<tze::Bundle<tze::Authorized>>> {
        let vin = Vector::read(&mut reader, TzeIn::read)?;
        let vout = Vector::read(&mut reader, TzeOut::read)?;
        Ok(if vin.is_empty() && vout.is_empty() {
            None
        } else {
            Some(tze::Bundle {
                vin,
                vout,
                authorization: tze::Authorized,
            })
        })
    }

    pub fn write<W: Write>(&self, writer: W) -> io::Result<()> {
        match self.version {
            TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
                self.write_v4(writer)
            }
            TxVersion::Zip225 => self.write_v5(writer),
            #[cfg(feature = "zfuture")]
            TxVersion::ZFuture => self.write_v5(writer),
        }
    }

    pub fn write_v4<W: Write>(&self, mut writer: W) -> io::Result<()> {
        self.version.write(&mut writer)?;

        self.write_transparent(&mut writer)?;
        writer.write_u32::<LittleEndian>(self.lock_time)?;
        if self.version.has_overwinter() {
            writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?;
        }

        if self.version.has_sapling() {
            writer.write_all(
                &self
                    .sapling_bundle
                    .as_ref()
                    .map_or(Amount::zero(), |b| *b.value_balance())
                    .to_i64_le_bytes(),
            )?;
            Vector::write(
                &mut writer,
                self.sapling_bundle
                    .as_ref()
                    .map_or(&[], |b| b.shielded_spends()),
                |w, e| e.write_v4(w),
            )?;
            Vector::write(
                &mut writer,
                self.sapling_bundle
                    .as_ref()
                    .map_or(&[], |b| b.shielded_outputs()),
                |w, e| e.write_v4(w),
            )?;
        } else if self.sapling_bundle.is_some() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Sapling components may not be present if Sapling is not active.",
            ));
        }

        if self.version.has_sprout() {
            if let Some(bundle) = self.sprout_bundle.as_ref() {
                Vector::write(&mut writer, &bundle.joinsplits, |w, e| e.write(w))?;
                writer.write_all(&bundle.joinsplit_pubkey)?;
                writer.write_all(&bundle.joinsplit_sig)?;
            } else {
                CompactSize::write(&mut writer, 0)?;
            }
        }

        if self.version.has_sapling() {
            if let Some(bundle) = self.sapling_bundle.as_ref() {
                bundle.authorization().binding_sig.write(&mut writer)?;
            }
        }

        if self.orchard_bundle.is_some() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Orchard components cannot be present when serializing to the V4 transaction format."
            ));
        }

        Ok(())
    }

    pub fn write_transparent<W: Write>(&self, mut writer: W) -> io::Result<()> {
        if let Some(bundle) = &self.transparent_bundle {
            Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?;
            Vector::write(&mut writer, &bundle.vout, |w, e| e.write(w))?;
        } else {
            CompactSize::write(&mut writer, 0)?;
            CompactSize::write(&mut writer, 0)?;
        }

        Ok(())
    }

    pub fn write_v5<W: Write>(&self, mut writer: W) -> io::Result<()> {
        if self.sprout_bundle.is_some() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Sprout components cannot be present when serializing to the V5 transaction format.",
            ));
        }
        self.write_v5_header(&mut writer)?;
        self.write_transparent(&mut writer)?;
        self.write_v5_sapling(&mut writer)?;
        orchard_serialization::write_v5_bundle(self.orchard_bundle.as_ref(), &mut writer)?;
        #[cfg(feature = "zfuture")]
        self.write_tze(&mut writer)?;
        Ok(())
    }

    pub fn write_v5_header<W: Write>(&self, mut writer: W) -> io::Result<()> {
        self.version.write(&mut writer)?;
        writer.write_u32::<LittleEndian>(u32::from(self.consensus_branch_id))?;
        writer.write_u32::<LittleEndian>(self.lock_time)?;
        writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?;
        Ok(())
    }

    pub fn write_v5_sapling<W: Write>(&self, mut writer: W) -> io::Result<()> {
        if let Some(bundle) = &self.sapling_bundle {
            Vector::write(&mut writer, bundle.shielded_spends(), |w, e| {
                e.write_v5_without_witness_data(w)
            })?;

            Vector::write(&mut writer, bundle.shielded_outputs(), |w, e| {
                e.write_v5_without_proof(w)
            })?;

            if !(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()) {
                writer.write_all(&bundle.value_balance().to_i64_le_bytes())?;
            }
            if !bundle.shielded_spends().is_empty() {
                writer.write_all(bundle.shielded_spends()[0].anchor().to_repr().as_ref())?;
            }

            Array::write(
                &mut writer,
                bundle.shielded_spends().iter().map(|s| &s.zkproof()[..]),
                |w, e| w.write_all(e),
            )?;
            Array::write(
                &mut writer,
                bundle.shielded_spends().iter().map(|s| s.spend_auth_sig()),
                |w, e| e.write(w),
            )?;

            Array::write(
                &mut writer,
                bundle.shielded_outputs().iter().map(|s| &s.zkproof()[..]),
                |w, e| w.write_all(e),
            )?;

            if !(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()) {
                bundle.authorization().binding_sig.write(&mut writer)?;
            }
        } else {
            CompactSize::write(&mut writer, 0)?;
            CompactSize::write(&mut writer, 0)?;
        }

        Ok(())
    }

    #[cfg(feature = "zfuture")]
    pub fn write_tze<W: Write>(&self, mut writer: W) -> io::Result<()> {
        if let Some(bundle) = &self.tze_bundle {
            Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?;
            Vector::write(&mut writer, &bundle.vout, |w, e| e.write(w))?;
        } else {
            CompactSize::write(&mut writer, 0)?;
            CompactSize::write(&mut writer, 0)?;
        }

        Ok(())
    }

    // TODO: should this be moved to `from_data` and stored?
    pub fn auth_commitment(&self) -> Blake2bHash {
        self.data.digest(BlockTxCommitmentDigester)
    }
}

#[derive(Clone, Debug)]
pub struct TransparentDigests<A> {
    pub prevouts_digest: A,
    pub sequence_digest: A,
    pub outputs_digest: A,
}

#[derive(Clone, Debug)]
pub struct TzeDigests<A> {
    pub inputs_digest: A,
    pub outputs_digest: A,
    pub per_input_digest: Option<A>,
}

#[derive(Clone, Debug)]
pub struct TxDigests<A> {
    pub header_digest: A,
    pub transparent_digests: Option<TransparentDigests<A>>,
    pub sapling_digest: Option<A>,
    pub orchard_digest: Option<A>,
    #[cfg(feature = "zfuture")]
    pub tze_digests: Option<TzeDigests<A>>,
}

pub trait TransactionDigest<A: Authorization> {
    type HeaderDigest;
    type TransparentDigest;
    type SaplingDigest;
    type OrchardDigest;

    #[cfg(feature = "zfuture")]
    type TzeDigest;

    type Digest;

    fn digest_header(
        &self,
        version: TxVersion,
        consensus_branch_id: BranchId,
        lock_time: u32,
        expiry_height: BlockHeight,
    ) -> Self::HeaderDigest;

    fn digest_transparent(
        &self,
        transparent_bundle: Option<&transparent::Bundle<A::TransparentAuth>>,
    ) -> Self::TransparentDigest;

    fn digest_sapling(
        &self,
        sapling_bundle: Option<&sapling::Bundle<A::SaplingAuth>>,
    ) -> Self::SaplingDigest;

    fn digest_orchard(
        &self,
        orchard_bundle: Option<&orchard::Bundle<A::OrchardAuth, Amount>>,
    ) -> Self::OrchardDigest;

    #[cfg(feature = "zfuture")]
    fn digest_tze(&self, tze_bundle: Option<&tze::Bundle<A::TzeAuth>>) -> Self::TzeDigest;

    fn combine(
        &self,
        header_digest: Self::HeaderDigest,
        transparent_digest: Self::TransparentDigest,
        sapling_digest: Self::SaplingDigest,
        orchard_digest: Self::OrchardDigest,
        #[cfg(feature = "zfuture")] tze_digest: Self::TzeDigest,
    ) -> Self::Digest;
}

pub enum DigestError {
    NotSigned,
}

#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
    use proptest::prelude::*;

    use crate::consensus::BranchId;

    use super::{
        components::{
            orchard::testing::{self as orchard},
            sapling::testing::{self as sapling},
            transparent::testing::{self as transparent},
        },
        Authorized, Transaction, TransactionData, TxId, TxVersion,
    };

    #[cfg(feature = "zfuture")]
    use super::components::tze::testing::{self as tze};

    pub fn arb_txid() -> impl Strategy<Value = TxId> {
        prop::array::uniform32(any::<u8>()).prop_map(TxId::from_bytes)
    }

    pub fn arb_tx_version(branch_id: BranchId) -> impl Strategy<Value = TxVersion> {
        match branch_id {
            BranchId::Sprout => (1..=2u32).prop_map(TxVersion::Sprout).boxed(),
            BranchId::Overwinter => Just(TxVersion::Overwinter).boxed(),
            BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => {
                Just(TxVersion::Sapling).boxed()
            }
            BranchId::Nu5 => Just(TxVersion::Zip225).boxed(),
            #[cfg(feature = "zfuture")]
            BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(),
        }
    }

    #[cfg(not(feature = "zfuture"))]
    prop_compose! {
        pub fn arb_txdata(consensus_branch_id: BranchId)(
            version in arb_tx_version(consensus_branch_id),
        )(
            lock_time in any::<u32>(),
            expiry_height in any::<u32>(),
            transparent_bundle in transparent::arb_bundle(),
            sapling_bundle in sapling::arb_bundle_for_version(version),
            orchard_bundle in orchard::arb_bundle_for_version(version),
            version in Just(version)
        ) -> TransactionData<Authorized> {
            TransactionData {
                version,
                consensus_branch_id,
                lock_time,
                expiry_height: expiry_height.into(),
                transparent_bundle,
                sprout_bundle: None,
                sapling_bundle,
                orchard_bundle
            }
        }
    }

    #[cfg(feature = "zfuture")]
    prop_compose! {
        pub fn arb_txdata(consensus_branch_id: BranchId)(
            version in arb_tx_version(consensus_branch_id),
        )(
            lock_time in any::<u32>(),
            expiry_height in any::<u32>(),
            transparent_bundle in transparent::arb_bundle(),
            sapling_bundle in sapling::arb_bundle_for_version(version),
            orchard_bundle in orchard::arb_bundle_for_version(version),
            tze_bundle in tze::arb_bundle(consensus_branch_id),
            version in Just(version)
        ) -> TransactionData<Authorized> {
            TransactionData {
                version,
                consensus_branch_id,
                lock_time,
                expiry_height: expiry_height.into(),
                transparent_bundle,
                sprout_bundle: None,
                sapling_bundle,
                orchard_bundle,
                tze_bundle
            }
        }
    }

    prop_compose! {
        pub fn arb_tx(branch_id: BranchId)(tx_data in arb_txdata(branch_id)) -> Transaction {
            Transaction::from_data(tx_data).unwrap()
        }
    }
}