use core::cmp::Ordering;
use core::fmt;
use rand_core::{CryptoRng, RngCore};
use ::sapling::{Note, PaymentAddress, builder::SaplingMetadata};
use ::transparent::{
address::TransparentAddress, builder::TransparentBuilder, bundle::TxOut, coinbase,
};
use zcash_protocol::{
PoolType,
consensus::{self, BlockHeight, BranchId, Parameters},
memo::MemoBytes,
value::{BalanceError, ZatBalance, Zatoshis},
};
use zcash_script::opcode::PushValue;
use crate::transaction::{
Transaction, TxVersion,
components::orchard::bundle_version_for_branch,
fees::{
FeeRule,
transparent::{InputView, OutputView},
},
};
#[cfg(feature = "std")]
use std::sync::mpsc::Sender;
#[cfg(feature = "circuits")]
use {
crate::transaction::{
Authorization, Coinbase, TransactionData, TxDigests, Unauthorized,
sighash::{SignableInput, signature_hash},
txid::TxIdDigester,
},
::sapling::prover::{OutputProver, SpendProver},
::transparent::builder::TransparentSigningSet,
alloc::vec::Vec,
};
#[cfg(feature = "transparent-inputs")]
use {::transparent::builder::TransparentInputInfo, zcash_script::script};
#[cfg(not(feature = "transparent-inputs"))]
use core::convert::Infallible;
use super::components::sapling::zip212_enforcement;
pub const DEFAULT_TX_EXPIRY_DELTA: u32 = 40;
#[derive(Debug)]
pub enum FeeError<FE> {
FeeRule(FE),
Bundle(&'static str),
}
impl<FE: fmt::Display> fmt::Display for FeeError<FE> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FeeError::FeeRule(e) => write!(f, "An error occurred in fee calculation: {e}"),
FeeError::Bundle(b) => write!(f, "Bundle structure invalid in fee calculation: {b}"),
}
}
}
#[derive(Debug)]
pub enum Error<FE> {
InsufficientFunds(ZatBalance),
ChangeRequired(ZatBalance),
Fee(FeeError<FE>),
Balance(BalanceError),
TransparentBuild(transparent::builder::Error),
SaplingBuild(sapling::builder::Error),
OrchardBuild(orchard::builder::BuildError),
IronwoodBuild(orchard::builder::BuildError),
OrchardSpend(orchard::builder::SpendError),
OrchardRecipient(orchard::builder::OutputError),
IronwoodSpend(orchard::builder::SpendError),
IronwoodSpendUnsupportedNoteVersion(orchard::NoteVersion),
IronwoodRecipient(orchard::builder::OutputError),
SaplingBuilderNotAvailable,
OrchardBuilderNotAvailable,
IronwoodBuilderNotAvailable,
AnchorDeferralUnsupported(TxVersion),
Coinbase(coinbase::Error),
CoinbaseExpiryHeightMismatch {
target_height: BlockHeight,
expiry_height: BlockHeight,
},
TargetIncompatible(BranchId, TxVersion, Option<PoolType>),
}
impl<FE: fmt::Display> fmt::Display for Error<FE> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::InsufficientFunds(amount) => write!(
f,
"Insufficient funds for transaction construction; need an additional {amount:?} zatoshis"
),
Error::ChangeRequired(amount) => write!(
f,
"The transaction requires an additional change output of {amount:?} zatoshis"
),
Error::Balance(e) => write!(f, "Invalid amount {e:?}"),
Error::Fee(e) => write!(f, "An error occurred in fee calculation: {e}"),
Error::TransparentBuild(err) => err.fmt(f),
Error::SaplingBuild(err) => err.fmt(f),
Error::OrchardBuild(err) => write!(f, "{err:?}"),
Error::IronwoodBuild(err) => write!(f, "{err:?}"),
Error::OrchardSpend(err) => write!(f, "Could not add Orchard spend: {err}"),
Error::OrchardRecipient(err) => write!(f, "Could not add Orchard recipient: {err}"),
Error::IronwoodSpend(err) => write!(f, "Could not add Ironwood spend: {err}"),
Error::IronwoodSpendUnsupportedNoteVersion(version) => write!(
f,
"Could not add Ironwood spend: note version {version:?} is unsupported"
),
Error::IronwoodRecipient(err) => {
write!(f, "Could not add Ironwood recipient: {err}")
}
Error::SaplingBuilderNotAvailable => write!(
f,
"Cannot create Sapling transactions without a Sapling anchor"
),
Error::OrchardBuilderNotAvailable => write!(
f,
"Cannot create Orchard transactions without an Orchard anchor, or before NU5 activation"
),
Error::IronwoodBuilderNotAvailable => write!(
f,
"Cannot create Ironwood transactions without an Ironwood anchor, or before NU6.3 activation"
),
Error::AnchorDeferralUnsupported(version) => write!(
f,
"Transaction version {version:?} commits its signatures to shielded anchors, so anchors cannot be deferred to proving time"
),
Error::Coinbase(err) => write!(
f,
"An error occurred in constructing a coinbase transaction: {err}"
),
Error::CoinbaseExpiryHeightMismatch {
target_height,
expiry_height,
} => write!(
f,
"Coinbase transaction expiry height {expiry_height} does not match target block height {target_height}"
),
Error::TargetIncompatible(branch_id, version, pool_type) => match pool_type {
None => write!(
f,
"Proposed transaction version {version:?} is not valid for consensus branch {branch_id:?}"
),
Some(t) => write!(
f,
"{t} is not supported for proposed transaction version {version:?} or consensus branch {branch_id:?}"
),
},
}
}
}
#[cfg(feature = "std")]
impl<FE: fmt::Debug + fmt::Display> std::error::Error for Error<FE> {}
impl<FE> From<BalanceError> for Error<FE> {
fn from(e: BalanceError) -> Self {
Error::Balance(e)
}
}
impl<FE> From<FeeError<FE>> for Error<FE> {
fn from(e: FeeError<FE>) -> Self {
Error::Fee(e)
}
}
impl<FE> From<sapling::builder::Error> for Error<FE> {
fn from(e: sapling::builder::Error) -> Self {
Error::SaplingBuild(e)
}
}
impl<FE> From<orchard::builder::SpendError> for Error<FE> {
fn from(e: orchard::builder::SpendError) -> Self {
Error::OrchardSpend(e)
}
}
impl<FE> From<coinbase::Error> for Error<FE> {
fn from(e: coinbase::Error) -> Self {
Error::Coinbase(e)
}
}
pub struct Progress {
cur: u32,
end: Option<u32>,
}
impl From<(u32, u32)> for Progress {
fn from((cur, end): (u32, u32)) -> Self {
Self {
cur,
end: Some(end),
}
}
}
impl Progress {
pub fn cur(&self) -> u32 {
self.cur
}
pub fn end(&self) -> Option<u32> {
self.end
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BundlePadding {
pub bundle_required: bool,
pub pad_to_minimum: Option<u8>,
}
impl BundlePadding {
pub const DEFAULT: BundlePadding = BundlePadding {
bundle_required: false,
pad_to_minimum: None,
};
pub const UNPADDED: BundlePadding = BundlePadding {
bundle_required: false,
pad_to_minimum: Some(1),
};
pub fn bundle_type(self) -> orchard::builder::BundleType {
orchard::builder::BundleType::Transactional {
bundle_required: self.bundle_required,
pad_to_minimum: self.pad_to_minimum,
}
}
}
#[derive(Clone)]
pub enum BuildConfig {
Standard {
sapling_anchor: Option<sapling::Anchor>,
orchard_anchor: Option<orchard::Anchor>,
ironwood_anchor: Option<orchard::Anchor>,
orchard_padding: BundlePadding,
ironwood_padding: BundlePadding,
},
Coinbase {
miner_data: Option<PushValue>,
},
}
impl BuildConfig {
pub fn sapling_builder_config(
&self,
) -> Option<(sapling::builder::BundleType, sapling::Anchor)> {
match self {
BuildConfig::Standard { sapling_anchor, .. } => sapling_anchor
.as_ref()
.map(|a| (sapling::builder::BundleType::DEFAULT, *a)),
BuildConfig::Coinbase { .. } => Some((
sapling::builder::BundleType::Coinbase,
sapling::Anchor::empty_tree(),
)),
}
}
fn orchard_builder(
&self,
bundle_version: orchard::bundle::BundleVersion,
) -> Option<orchard::builder::Builder> {
match self {
BuildConfig::Standard {
orchard_anchor,
orchard_padding,
..
} => orchard_anchor.as_ref().map(|a| {
orchard::builder::Builder::new(
orchard_padding.bundle_type(),
bundle_version,
bundle_version.default_flags(),
*a,
)
.expect("a transactional bundle type with default flags is always representable")
}),
BuildConfig::Coinbase { .. }
if bundle_version == orchard::bundle::BundleVersion::orchard_v3() =>
{
None
}
BuildConfig::Coinbase { .. } => Some(
orchard::builder::Builder::new(
orchard::builder::BundleType::Coinbase,
bundle_version,
orchard::bundle::Flags::SPENDS_DISABLED,
orchard::Anchor::empty_tree(),
)
.expect("spends-disabled flags are valid for a non-Orchard coinbase bundle"),
),
}
}
fn ironwood_builder(&self) -> Option<orchard::builder::Builder> {
let bundle_version = orchard::bundle::BundleVersion::ironwood_v3();
match self {
BuildConfig::Standard {
ironwood_anchor,
ironwood_padding,
..
} => ironwood_anchor.as_ref().map(|a| {
orchard::builder::Builder::new(
ironwood_padding.bundle_type(),
bundle_version,
bundle_version.default_flags(),
*a,
)
.expect("a transactional bundle type with default flags is always representable")
}),
BuildConfig::Coinbase { .. } => Some(
orchard::builder::Builder::new(
orchard::builder::BundleType::Coinbase,
bundle_version,
orchard::bundle::Flags::SPENDS_DISABLED,
orchard::Anchor::empty_tree(),
)
.expect("spends-disabled flags are valid for an Ironwood coinbase bundle"),
),
}
}
pub fn is_coinbase(&self) -> bool {
matches!(self, BuildConfig::Coinbase { .. })
}
}
fn orchard_action_count(
builder: &orchard::builder::Builder,
is_coinbase: bool,
bundle_version: orchard::bundle::BundleVersion,
) -> Result<usize, &'static str> {
let num_spends = builder.spends().len();
let num_outputs = builder
.outputs()
.len()
.checked_add(builder.changes().len())
.ok_or("num_outputs + num_changes overflowed")?;
let bundle_type = builder.bundle_type();
let flags = if is_coinbase {
orchard::bundle::Flags::SPENDS_DISABLED
} else {
bundle_version.default_flags()
};
bundle_type.num_actions(flags, num_spends, num_outputs)
}
pub struct DeferredPcztBuilder<P> {
params: P,
tx_version: TxVersion,
consensus_branch_id: BranchId,
target_height: BlockHeight,
expiry_height: BlockHeight,
orchard_builder: orchard::builder::Builder,
orchard_bundle_version: orchard::bundle::BundleVersion,
ironwood_builder: orchard::builder::Builder,
}
impl<P: consensus::Parameters> DeferredPcztBuilder<P> {
pub fn new<FE>(
params: P,
target_height: BlockHeight,
orchard_padding: BundlePadding,
ironwood_padding: BundlePadding,
) -> Result<Self, Error<FE>> {
let consensus_branch_id = BranchId::for_height(¶ms, target_height);
let tx_version = TxVersion::suggested_for_branch(consensus_branch_id);
if !tx_version.has_ironwood() {
return Err(Error::AnchorDeferralUnsupported(tx_version));
}
let orchard_bundle_version =
bundle_version_for_branch(consensus_branch_id, orchard::ValuePool::Orchard)
.expect("a branch with the V6 format supports the Orchard pool");
let orchard_builder = orchard::builder::Builder::new_with_anchor_deferred(
orchard_padding.bundle_type(),
orchard_bundle_version,
orchard_bundle_version.default_flags(),
orchard::bundle::TxVersion::V6,
)
.map_err(Error::OrchardBuild)?;
let ironwood_bundle_version = orchard::bundle::BundleVersion::ironwood_v3();
let ironwood_builder = orchard::builder::Builder::new_with_anchor_deferred(
ironwood_padding.bundle_type(),
ironwood_bundle_version,
ironwood_bundle_version.default_flags(),
orchard::bundle::TxVersion::V6,
)
.map_err(Error::IronwoodBuild)?;
Ok(DeferredPcztBuilder {
params,
tx_version,
consensus_branch_id,
target_height,
expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA,
orchard_builder,
orchard_bundle_version,
ironwood_builder,
})
}
pub fn with_expiry_height(mut self, expiry_height: BlockHeight) -> Self {
self.expiry_height = expiry_height;
self
}
pub fn add_orchard_spend<FE>(
&mut self,
fvk: orchard::keys::FullViewingKey,
note: orchard::Note,
) -> Result<(), Error<FE>> {
self.orchard_builder.add_spend_unwitnessed(fvk, note)?;
Ok(())
}
pub fn add_orchard_output<FE>(
&mut self,
ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.orchard_builder
.add_output(
ovk,
recipient,
orchard::value::NoteValue::from_raw(value.into()),
memo.into_bytes(),
)
.map_err(Error::OrchardRecipient)
}
pub fn add_orchard_change_output<FE>(
&mut self,
fvk: orchard::keys::FullViewingKey,
ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.orchard_builder
.add_change_output(
fvk,
ovk,
recipient,
orchard::value::NoteValue::from_raw(value.into()),
memo.into_bytes(),
)
.map_err(Error::OrchardRecipient)
}
pub fn add_ironwood_spend<FE>(
&mut self,
fvk: orchard::keys::FullViewingKey,
note: orchard::Note,
) -> Result<(), Error<FE>> {
if note.version() != orchard::note::NoteVersion::V3 {
return Err(Error::IronwoodSpendUnsupportedNoteVersion(note.version()));
}
self.ironwood_builder
.add_spend_unwitnessed(fvk, note)
.map_err(Error::IronwoodSpend)
}
pub fn add_ironwood_output<FE>(
&mut self,
ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.ironwood_builder
.add_output(
ovk,
recipient,
orchard::value::NoteValue::from_raw(value.into()),
memo.into_bytes(),
)
.map_err(Error::IronwoodRecipient)
}
pub fn get_fee<FR: FeeRule>(&self, fee_rule: &FR) -> Result<Zatoshis, FeeError<FR::Error>> {
fee_rule
.fee_required(
&self.params,
self.target_height,
core::iter::empty::<crate::transaction::fees::transparent::InputSize>(),
core::iter::empty::<usize>(),
0,
0,
orchard_action_count(&self.orchard_builder, false, self.orchard_bundle_version)
.map_err(FeeError::Bundle)?,
orchard_action_count(
&self.ironwood_builder,
false,
orchard::bundle::BundleVersion::ironwood_v3(),
)
.map_err(FeeError::Bundle)?,
)
.map_err(FeeError::FeeRule)
}
pub fn build_for_pczt<R: RngCore + CryptoRng, FR: FeeRule>(
self,
mut rng: R,
fee_rule: &FR,
) -> Result<PcztResult<P>, Error<FR::Error>> {
fn in_use(builder: &orchard::builder::Builder) -> bool {
!builder.spends().is_empty()
|| !builder.outputs().is_empty()
|| !builder.changes().is_empty()
}
let fee = self.get_fee(fee_rule).map_err(Error::Fee)?;
let value_balance = [
self.orchard_builder
.value_balance::<ZatBalance>()
.map_err(|_| BalanceError::Overflow)?,
self.ironwood_builder
.value_balance::<ZatBalance>()
.map_err(|_| BalanceError::Overflow)?,
]
.into_iter()
.sum::<Option<ZatBalance>>()
.ok_or(BalanceError::Overflow)?;
let balance_after_fees = (value_balance - fee).ok_or(BalanceError::Underflow)?;
match balance_after_fees.cmp(&ZatBalance::zero()) {
Ordering::Less => {
return Err(Error::InsufficientFunds(-balance_after_fees));
}
Ordering::Greater => {
return Err(Error::ChangeRequired(balance_after_fees));
}
Ordering::Equal => (),
};
let (orchard_bundle, orchard_meta) = if in_use(&self.orchard_builder) {
let (bundle, meta) = self
.orchard_builder
.build_for_pczt(&mut rng)
.map_err(Error::OrchardBuild)?;
(Some(bundle), meta)
} else {
(None, orchard::builder::BundleMetadata::empty())
};
let (ironwood_bundle, ironwood_meta) = if in_use(&self.ironwood_builder) {
let (bundle, meta) = self
.ironwood_builder
.build_for_pczt(&mut rng)
.map_err(Error::IronwoodBuild)?;
(Some(bundle), meta)
} else {
(None, orchard::builder::BundleMetadata::empty())
};
Ok(PcztResult {
pczt_parts: PcztParts {
params: self.params,
version: self.tx_version,
consensus_branch_id: self.consensus_branch_id,
lock_time: 0,
expiry_height: self.expiry_height,
transparent: None,
sapling: None,
orchard: orchard_bundle,
ironwood: ironwood_bundle,
},
sapling_meta: SaplingMetadata::empty(),
orchard_meta,
ironwood_meta,
})
}
}
#[derive(Debug)]
pub struct BuildResult {
transaction: Transaction,
sapling_meta: SaplingMetadata,
orchard_meta: orchard::builder::BundleMetadata,
ironwood_meta: orchard::builder::BundleMetadata,
}
impl BuildResult {
pub fn transaction(&self) -> &Transaction {
&self.transaction
}
pub fn sapling_meta(&self) -> &SaplingMetadata {
&self.sapling_meta
}
pub fn orchard_meta(&self) -> &orchard::builder::BundleMetadata {
&self.orchard_meta
}
pub fn ironwood_meta(&self) -> &orchard::builder::BundleMetadata {
&self.ironwood_meta
}
}
#[derive(Debug)]
pub struct PcztResult<P: Parameters> {
pub pczt_parts: PcztParts<P>,
pub sapling_meta: SaplingMetadata,
pub orchard_meta: orchard::builder::BundleMetadata,
pub ironwood_meta: orchard::builder::BundleMetadata,
}
#[derive(Debug)]
pub struct PcztParts<P: Parameters> {
pub params: P,
pub version: TxVersion,
pub consensus_branch_id: BranchId,
pub lock_time: u32,
pub expiry_height: BlockHeight,
pub transparent: Option<transparent::pczt::Bundle>,
pub sapling: Option<sapling::pczt::Bundle>,
pub orchard: Option<orchard::pczt::Bundle>,
pub ironwood: Option<orchard::pczt::Bundle>,
}
pub struct Builder<P, U> {
params: P,
tx_version: TxVersion,
consensus_branch_id: BranchId,
build_config: BuildConfig,
target_height: BlockHeight,
expiry_height: BlockHeight,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
zip233_amount: Zatoshis,
transparent_builder: TransparentBuilder,
sapling_builder: Option<sapling::builder::Builder>,
orchard_builder: Option<orchard::builder::Builder>,
orchard_bundle_version: Option<orchard::bundle::BundleVersion>,
ironwood_builder: Option<orchard::builder::Builder>,
_progress_notifier: U,
}
impl<P, U> Builder<P, U> {
pub fn params(&self) -> &P {
&self.params
}
pub fn target_height(&self) -> BlockHeight {
self.target_height
}
#[cfg(feature = "transparent-inputs")]
pub fn transparent_inputs(&self) -> &[TransparentInputInfo] {
self.transparent_builder.inputs()
}
pub fn transparent_outputs(&self) -> &[TxOut] {
self.transparent_builder.outputs()
}
pub fn sapling_inputs(&self) -> &[sapling::builder::SpendInfo] {
self.sapling_builder
.as_ref()
.map_or_else(|| &[][..], |b| b.inputs())
}
pub fn sapling_outputs(&self) -> &[sapling::builder::OutputInfo] {
self.sapling_builder
.as_ref()
.map_or_else(|| &[][..], |b| b.outputs())
}
fn orchard_in_use(&self) -> bool {
self.orchard_builder.as_ref().is_some_and(|b| {
!b.spends().is_empty() || !b.outputs().is_empty() || !b.changes().is_empty()
})
}
fn ironwood_in_use(&self) -> bool {
self.ironwood_builder.as_ref().is_some_and(|b| {
!b.spends().is_empty() || !b.outputs().is_empty() || !b.changes().is_empty()
})
}
fn check_version_compatibility<FE>(&self, version: TxVersion) -> Result<(), Error<FE>> {
if !version.valid_in_branch(self.consensus_branch_id) {
return Err(Error::TargetIncompatible(
self.consensus_branch_id,
version,
None,
));
}
let sapling_available = version.has_sapling() && self.consensus_branch_id.has_sapling();
if !sapling_available
&& (!self.sapling_inputs().is_empty() || !self.sapling_outputs().is_empty())
{
return Err(Error::TargetIncompatible(
self.consensus_branch_id,
version,
Some(PoolType::SAPLING),
));
}
let orchard_available = version.has_orchard() && self.consensus_branch_id.has_orchard();
if !orchard_available && self.orchard_in_use() {
return Err(Error::TargetIncompatible(
self.consensus_branch_id,
version,
Some(PoolType::ORCHARD),
));
}
{
let ironwood_branch = match self.consensus_branch_id {
BranchId::Nu6_3 => true,
#[cfg(zcash_unstable = "nu7")]
BranchId::Nu7 => true,
_ => false,
};
let ironwood_available = version.has_ironwood() && ironwood_branch;
if !ironwood_available && self.ironwood_in_use() {
return Err(Error::TargetIncompatible(
self.consensus_branch_id,
version,
None,
));
}
}
Ok(())
}
pub fn propose_version<FE>(&mut self, version: TxVersion) -> Result<(), Error<FE>> {
self.check_version_compatibility(version)?;
self.tx_version = version;
Ok(())
}
}
impl<P: consensus::Parameters> Builder<P, ()> {
pub fn new(params: P, target_height: BlockHeight, build_config: BuildConfig) -> Self {
let consensus_branch_id = BranchId::for_height(¶ms, target_height);
let bundle_version =
bundle_version_for_branch(consensus_branch_id, orchard::ValuePool::Orchard);
let tx_version = TxVersion::suggested_for_branch(consensus_branch_id);
let orchard_builder = bundle_version.and_then(|v| build_config.orchard_builder(v));
let orchard_bundle_version = orchard_builder.as_ref().and(bundle_version);
let ironwood_builder = if tx_version.has_ironwood() {
build_config.ironwood_builder()
} else {
None
};
let sapling_builder = build_config
.sapling_builder_config()
.map(|(bundle_type, anchor)| {
sapling::builder::Builder::new(
zip212_enforcement(¶ms, target_height),
bundle_type,
anchor,
)
});
let expiry_height = if build_config.is_coinbase() {
target_height
} else {
target_height + DEFAULT_TX_EXPIRY_DELTA
};
Builder {
params,
tx_version,
consensus_branch_id,
build_config,
target_height,
expiry_height,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
zip233_amount: Zatoshis::ZERO,
transparent_builder: TransparentBuilder::empty(),
sapling_builder,
orchard_builder,
orchard_bundle_version,
ironwood_builder,
_progress_notifier: (),
}
}
#[cfg(feature = "std")]
pub fn with_progress_notifier(
self,
_progress_notifier: Sender<Progress>,
) -> Builder<P, Sender<Progress>> {
Builder {
params: self.params,
tx_version: self.tx_version,
consensus_branch_id: self.consensus_branch_id,
build_config: self.build_config,
target_height: self.target_height,
expiry_height: self.expiry_height,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
zip233_amount: self.zip233_amount,
transparent_builder: self.transparent_builder,
sapling_builder: self.sapling_builder,
orchard_builder: self.orchard_builder,
orchard_bundle_version: self.orchard_bundle_version,
ironwood_builder: self.ironwood_builder,
_progress_notifier,
}
}
}
impl<P: consensus::Parameters, U> Builder<P, U> {
pub fn with_expiry_height(mut self, expiry_height: BlockHeight) -> Self {
self.expiry_height = expiry_height;
self
}
fn check_coinbase_expiry_height<FE>(&self) -> Result<(), Error<FE>> {
if self.build_config.is_coinbase() && self.expiry_height != self.target_height {
Err(Error::CoinbaseExpiryHeightMismatch {
target_height: self.target_height,
expiry_height: self.expiry_height,
})
} else {
Ok(())
}
}
pub fn add_orchard_spend<FE>(
&mut self,
fvk: orchard::keys::FullViewingKey,
note: orchard::Note,
merkle_path: orchard::tree::MerklePath,
) -> Result<(), Error<FE>> {
if let Some(builder) = self.orchard_builder.as_mut() {
builder.add_spend(fvk, note, merkle_path)?;
Ok(())
} else {
Err(Error::OrchardBuilderNotAvailable)
}
}
pub fn add_orchard_output<FE>(
&mut self,
ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.orchard_builder
.as_mut()
.ok_or(Error::OrchardBuilderNotAvailable)?
.add_output(
ovk,
recipient,
orchard::value::NoteValue::from_raw(value.into()),
memo.into_bytes(),
)
.map_err(Error::OrchardRecipient)
}
pub fn add_orchard_change_output<FE>(
&mut self,
fvk: orchard::keys::FullViewingKey,
ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.orchard_builder
.as_mut()
.ok_or(Error::OrchardBuilderNotAvailable)?
.add_change_output(
fvk,
ovk,
recipient,
orchard::value::NoteValue::from_raw(value.into()),
memo.into_bytes(),
)
.map_err(Error::OrchardRecipient)
}
pub fn add_ironwood_spend<FE>(
&mut self,
fvk: orchard::keys::FullViewingKey,
note: orchard::Note,
merkle_path: orchard::tree::MerklePath,
) -> Result<(), Error<FE>> {
let builder = self
.ironwood_builder
.as_mut()
.ok_or(Error::IronwoodBuilderNotAvailable)?;
if note.version() != orchard::note::NoteVersion::V3 {
return Err(Error::IronwoodSpendUnsupportedNoteVersion(note.version()));
}
builder
.add_spend(fvk, note, merkle_path)
.map_err(Error::IronwoodSpend)?;
Ok(())
}
pub fn add_ironwood_output<FE>(
&mut self,
ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.ironwood_builder
.as_mut()
.ok_or(Error::IronwoodBuilderNotAvailable)?
.add_output(
ovk,
recipient,
orchard::value::NoteValue::from_raw(value.into()),
memo.into_bytes(),
)
.map_err(Error::IronwoodRecipient)
}
pub fn add_sapling_spend<FE>(
&mut self,
fvk: sapling::keys::FullViewingKey,
note: Note,
merkle_path: sapling::MerklePath,
) -> Result<(), Error<FE>> {
if let Some(builder) = self.sapling_builder.as_mut() {
builder.add_spend(fvk, note, merkle_path)?;
Ok(())
} else {
Err(Error::SaplingBuilderNotAvailable)
}
}
pub fn add_sapling_output<FE>(
&mut self,
ovk: Option<sapling::keys::OutgoingViewingKey>,
to: PaymentAddress,
value: Zatoshis,
memo: MemoBytes,
) -> Result<(), Error<FE>> {
self.sapling_builder
.as_mut()
.ok_or(Error::SaplingBuilderNotAvailable)?
.add_output(
ovk,
to,
sapling::value::NoteValue::from_raw(u64::from(value)),
memo.into_bytes(),
)
.map_err(Error::SaplingBuild)
}
#[cfg(feature = "transparent-inputs")]
pub fn add_transparent_input(&mut self, input: TransparentInputInfo) {
self.transparent_builder.add_input(input)
}
#[cfg(feature = "transparent-inputs")]
pub fn add_transparent_p2pkh_input(
&mut self,
pubkey: secp256k1::PublicKey,
utxo: transparent::bundle::OutPoint,
coin: TxOut,
) -> Result<(), transparent::builder::Error> {
self.transparent_builder.add_p2pkh_input(pubkey, utxo, coin)
}
#[cfg(feature = "transparent-inputs")]
pub fn add_transparent_p2sh_input(
&mut self,
redeem_script: script::FromChain,
utxo: transparent::bundle::OutPoint,
coin: TxOut,
) -> Result<(), transparent::builder::Error> {
self.transparent_builder
.add_p2sh_input(redeem_script, utxo, coin)
}
pub fn add_transparent_output(
&mut self,
to: &TransparentAddress,
value: Zatoshis,
) -> Result<(), transparent::builder::Error> {
self.transparent_builder.add_output(to, value)
}
pub fn add_transparent_null_data_output<FE>(&mut self, data: &[u8]) -> Result<(), Error<FE>> {
self.transparent_builder
.add_null_data_output(data)
.map_err(Error::TransparentBuild)
}
fn value_balance(&self) -> Result<ZatBalance, BalanceError> {
let value_balances = [
self.transparent_builder.value_balance()?,
self.sapling_builder
.as_ref()
.map_or_else(ZatBalance::zero, |builder| {
builder.value_balance::<ZatBalance>()
}),
self.orchard_builder.as_ref().map_or_else(
|| Ok(ZatBalance::zero()),
|builder| {
builder
.value_balance::<ZatBalance>()
.map_err(|_| BalanceError::Overflow)
},
)?,
self.ironwood_builder.as_ref().map_or_else(
|| Ok(ZatBalance::zero()),
|builder| {
builder
.value_balance::<ZatBalance>()
.map_err(|_| BalanceError::Overflow)
},
)?,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
-ZatBalance::from(self.zip233_amount),
];
value_balances
.into_iter()
.sum::<Option<_>>()
.ok_or(BalanceError::Overflow)
}
pub fn get_fee<FR: FeeRule>(&self, fee_rule: &FR) -> Result<Zatoshis, FeeError<FR::Error>> {
#[cfg(feature = "transparent-inputs")]
let transparent_inputs = self.transparent_builder.inputs();
#[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs: &[Infallible] = &[];
let sapling_spends = self
.sapling_builder
.as_ref()
.map_or(0, |builder| builder.inputs().len());
let ironwood_actions = self
.ironwood_builder
.as_ref()
.map_or(Ok(0), |builder| {
orchard_action_count(
builder,
self.build_config.is_coinbase(),
orchard::bundle::BundleVersion::ironwood_v3(),
)
})
.map_err(FeeError::Bundle)?;
fee_rule
.fee_required(
&self.params,
self.target_height,
transparent_inputs.iter().map(|i| i.serialized_size()),
self.transparent_builder
.outputs()
.iter()
.map(|i| i.serialized_size()),
sapling_spends,
self.sapling_builder
.as_ref()
.zip(self.build_config.sapling_builder_config())
.map_or(Ok(0), |(builder, (bundle_type, _))| {
bundle_type
.num_outputs(sapling_spends, builder.outputs().len())
.map_err(FeeError::Bundle)
})?,
self.orchard_builder
.as_ref()
.map_or(Ok(0), |builder| {
orchard_action_count(
builder,
self.build_config.is_coinbase(),
self.orchard_bundle_version
.expect("orchard builder present implies bundle version"),
)
})
.map_err(FeeError::Bundle)?,
ironwood_actions,
)
.map_err(FeeError::FeeRule)
}
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
pub fn set_zip233_amount(&mut self, zip233_amount: Zatoshis) {
self.zip233_amount = zip233_amount;
}
}
impl<P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder<P, U> {
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "circuits")]
pub fn build<R: RngCore + CryptoRng, SP: SpendProver, OP: OutputProver, FR: FeeRule>(
self,
transparent_signing_set: &TransparentSigningSet,
sapling_extsks: &[sapling::zip32::ExtendedSpendingKey],
orchard_saks: &[orchard::keys::SpendAuthorizingKey],
rng: R,
spend_prover: &SP,
output_prover: &OP,
fee_rule: &FR,
) -> Result<BuildResult, Error<FR::Error>> {
match &self.build_config {
BuildConfig::Coinbase { miner_data } => {
let target_height = self.target_height;
let miner_data = miner_data.clone();
self.build_internal::<Coinbase, _, _, _, _>(
|b| {
b.build_coinbase(target_height, miner_data)
.map(Some)
.map_err(Error::Coinbase)
},
|b, _, _| Ok(b.clone().map_authorization(transparent::builder::Coinbase)),
&[],
&[],
rng,
spend_prover,
output_prover,
None,
)
}
BuildConfig::Standard { .. } => {
let fee = self.get_fee(fee_rule).map_err(Error::Fee)?;
self.build_internal::<Unauthorized, _, _, _, _>(
|b| Ok(b.build()),
|b, unauthed_tx, txid_parts| {
authorize_transparent(b, unauthed_tx, txid_parts, transparent_signing_set)
},
sapling_extsks,
orchard_saks,
rng,
spend_prover,
output_prover,
Some(fee),
)
}
}
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "circuits")]
fn build_internal<A, R, SP, OP, FE>(
self,
build_transparent: impl FnOnce(
TransparentBuilder,
) -> Result<
Option<transparent::bundle::Bundle<A::TransparentAuth>>,
Error<FE>,
>,
authorize_transparent: impl FnOnce(
&transparent::bundle::Bundle<A::TransparentAuth>,
&TransactionData<A>,
&TxDigests<blake2b_simd::Hash>,
) -> Result<
transparent::bundle::Bundle<transparent::bundle::Authorized>,
transparent::builder::Error,
>,
sapling_extsks: &[sapling::zip32::ExtendedSpendingKey],
orchard_saks: &[orchard::keys::SpendAuthorizingKey],
mut rng: R,
spend_prover: &SP,
output_prover: &OP,
fee: Option<Zatoshis>,
) -> Result<BuildResult, Error<FE>>
where
A: Authorization<
SaplingAuth = sapling::builder::InProgress<
sapling::builder::Proven,
sapling::builder::Unsigned,
>,
OrchardAuth = orchard::builder::InProgress<
orchard::builder::Unproven,
orchard::builder::Unauthorized,
>,
>,
A::TransparentAuth: transparent::sighash::TransparentAuthorizingContext,
R: RngCore + CryptoRng,
SP: SpendProver,
OP: OutputProver,
{
self.check_version_compatibility::<FE>(self.tx_version)?;
self.check_coinbase_expiry_height::<FE>()?;
assert_eq!(self.build_config.is_coinbase(), fee.is_none());
if let Some(fee) = fee {
let balance_after_fees =
(self.value_balance()? - fee).ok_or(BalanceError::Underflow)?;
match balance_after_fees.cmp(&ZatBalance::zero()) {
Ordering::Less => {
return Err(Error::InsufficientFunds(-balance_after_fees));
}
Ordering::Greater => {
return Err(Error::ChangeRequired(balance_after_fees));
}
Ordering::Equal => (),
};
}
let transparent_bundle = build_transparent(self.transparent_builder)?;
let (sapling_bundle, sapling_meta) = match self
.sapling_builder
.and_then(|builder| {
builder
.build::<SP, OP, _, _>(sapling_extsks, &mut rng)
.map_err(Error::SaplingBuild)
.transpose()
.map(|res| {
res.map(|(bundle, sapling_meta)| {
(
bundle.create_proofs(
spend_prover,
output_prover,
&mut rng,
self._progress_notifier,
),
sapling_meta,
)
})
})
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, SaplingMetadata::empty()),
};
let (orchard_bundle, orchard_meta) = match self
.orchard_builder
.and_then(|builder| {
builder
.build(&mut rng)
.map_err(Error::OrchardBuild)
.transpose()
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, orchard::builder::BundleMetadata::empty()),
};
let (ironwood_bundle, ironwood_meta) = match self
.ironwood_builder
.and_then(|builder| {
builder
.build(&mut rng)
.map_err(Error::IronwoodBuild)
.transpose()
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, orchard::builder::BundleMetadata::empty()),
};
let unauthed_tx: TransactionData<A> = TransactionData {
version: self.tx_version,
consensus_branch_id: self.consensus_branch_id,
lock_time: 0,
expiry_height: self.expiry_height,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
zip233_amount: self.zip233_amount,
transparent_bundle,
sprout_bundle: None,
sapling_bundle,
orchard_bundle,
ironwood_bundle,
};
let txid_parts = unauthed_tx.digest(TxIdDigester);
let transparent_bundle = unauthed_tx
.transparent_bundle
.as_ref()
.map(|b| authorize_transparent(b, &unauthed_tx, &txid_parts))
.transpose()
.map_err(Error::TransparentBuild)?;
let shielded_sig_commitment =
signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
let sapling_asks = sapling_extsks
.iter()
.map(|extsk| extsk.expsk.ask.clone())
.collect::<Vec<_>>();
let sapling_bundle = unauthed_tx
.sapling_bundle
.map(|b| b.apply_signatures(&mut rng, *shielded_sig_commitment.as_ref(), &sapling_asks))
.transpose()
.map_err(Error::SaplingBuild)?;
let orchard_circuit_version = {
let build_proving_key =
unauthed_tx.orchard_bundle.is_some() || unauthed_tx.ironwood_bundle.is_some();
build_proving_key.then(|| {
bundle_version_for_branch(
unauthed_tx.consensus_branch_id,
orchard::ValuePool::Orchard,
)
.expect("an Orchard or Ironwood bundle implies an NU5+ consensus branch")
.circuit_version()
})
};
#[cfg(feature = "std")]
let orchard_proving_key: Option<&orchard::circuit::ProvingKey> =
orchard_circuit_version.map(cached_orchard_proving_key);
#[cfg(not(feature = "std"))]
let orchard_proving_key_storage =
orchard_circuit_version.map(orchard::circuit::ProvingKey::build);
#[cfg(not(feature = "std"))]
let orchard_proving_key: Option<&orchard::circuit::ProvingKey> =
orchard_proving_key_storage.as_ref();
let orchard_bundle = unauthed_tx
.orchard_bundle
.map(|b| {
b.create_proof(
orchard_proving_key
.expect("proving key is built when an Orchard bundle is present"),
&mut rng,
)
.and_then(|b| {
b.apply_signatures(&mut rng, *shielded_sig_commitment.as_ref(), orchard_saks)
})
})
.transpose()
.map_err(Error::OrchardBuild)?;
let ironwood_bundle = unauthed_tx
.ironwood_bundle
.map(|b| {
b.create_proof(
orchard_proving_key
.expect("proving key is built when an Ironwood bundle is present"),
&mut rng,
)
.and_then(|b| {
b.apply_signatures(&mut rng, *shielded_sig_commitment.as_ref(), orchard_saks)
})
})
.transpose()
.map_err(Error::IronwoodBuild)?;
let authorized_tx = TransactionData {
version: unauthed_tx.version,
consensus_branch_id: unauthed_tx.consensus_branch_id,
lock_time: unauthed_tx.lock_time,
expiry_height: unauthed_tx.expiry_height,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
zip233_amount: unauthed_tx.zip233_amount,
transparent_bundle,
sprout_bundle: unauthed_tx.sprout_bundle,
sapling_bundle,
orchard_bundle,
ironwood_bundle,
};
Ok(BuildResult {
transaction: authorized_tx.freeze().unwrap(),
sapling_meta,
orchard_meta,
ironwood_meta,
})
}
}
impl<P: consensus::Parameters, U> Builder<P, U> {
pub fn build_for_pczt<R: RngCore + CryptoRng, FR: FeeRule>(
self,
mut rng: R,
fee_rule: &FR,
) -> Result<PcztResult<P>, Error<FR::Error>> {
let fee = self.get_fee(fee_rule).map_err(Error::Fee)?;
self.check_version_compatibility::<FR::Error>(self.tx_version)?;
self.check_coinbase_expiry_height::<FR::Error>()?;
let balance_after_fees = (self.value_balance()? - fee).ok_or(BalanceError::Underflow)?;
match balance_after_fees.cmp(&ZatBalance::zero()) {
Ordering::Less => {
return Err(Error::InsufficientFunds(-balance_after_fees));
}
Ordering::Greater => {
return Err(Error::ChangeRequired(balance_after_fees));
}
Ordering::Equal => (),
};
let transparent_bundle = self.transparent_builder.build_for_pczt();
let (sapling_bundle, sapling_meta) = match self
.sapling_builder
.map(|builder| {
builder
.build_for_pczt(&mut rng)
.map_err(Error::SaplingBuild)
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, SaplingMetadata::empty()),
};
let (orchard_bundle, orchard_meta) = match self
.orchard_builder
.map(|builder| {
builder
.build_for_pczt(&mut rng)
.map_err(Error::OrchardBuild)
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, orchard::builder::BundleMetadata::empty()),
};
let (ironwood_bundle, ironwood_meta) = if self.tx_version.has_ironwood() {
match self
.ironwood_builder
.map(|builder| {
builder
.build_for_pczt(&mut rng)
.map_err(Error::IronwoodBuild)
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, orchard::builder::BundleMetadata::empty()),
}
} else {
(None, orchard::builder::BundleMetadata::empty())
};
Ok(PcztResult {
pczt_parts: PcztParts {
params: self.params,
version: self.tx_version,
consensus_branch_id: self.consensus_branch_id,
lock_time: 0,
expiry_height: self.expiry_height,
transparent: transparent_bundle,
sapling: sapling_bundle,
orchard: orchard_bundle,
ironwood: ironwood_bundle,
},
sapling_meta,
orchard_meta,
ironwood_meta,
})
}
}
#[cfg(all(feature = "circuits", feature = "std"))]
pub fn cached_orchard_proving_key(
circuit_version: orchard::circuit::OrchardCircuitVersion,
) -> &'static orchard::circuit::ProvingKey {
use orchard::circuit::{OrchardCircuitVersion, ProvingKey};
use std::sync::OnceLock;
static INSECURE_PRE_NU6_2: OnceLock<ProvingKey> = OnceLock::new();
static FIXED_POST_NU6_2: OnceLock<ProvingKey> = OnceLock::new();
static POST_NU6_3: OnceLock<ProvingKey> = OnceLock::new();
let cell = match circuit_version {
OrchardCircuitVersion::InsecurePreNu6_2 => &INSECURE_PRE_NU6_2,
OrchardCircuitVersion::FixedPostNu6_2 => &FIXED_POST_NU6_2,
OrchardCircuitVersion::PostNu6_3 => &POST_NU6_3,
};
cell.get_or_init(|| ProvingKey::build(circuit_version))
}
#[cfg(feature = "circuits")]
fn authorize_transparent(
b: &transparent::bundle::Bundle<transparent::builder::Unauthorized>,
unauthed_tx: &TransactionData<Unauthorized>,
txid_parts: &TxDigests<blake2b_simd::Hash>,
transparent_signing_set: &TransparentSigningSet,
) -> Result<transparent::bundle::Bundle<transparent::bundle::Authorized>, transparent::builder::Error>
{
b.clone().apply_signatures(
|input| {
*signature_hash(unauthed_tx, &SignableInput::Transparent(input), txid_parts).as_ref()
},
transparent_signing_set,
)
}
#[cfg(all(any(test, feature = "test-dependencies"), feature = "circuits"))]
mod testing {
use rand_core::{CryptoRng, RngCore};
use ::sapling::prover::mock::{MockOutputProver, MockSpendProver};
use ::transparent::builder::TransparentSigningSet;
use zcash_protocol::consensus;
use super::{BuildResult, Builder, Error};
use crate::transaction::fees::zip317;
impl<P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder<P, U> {
pub fn mock_build<R: RngCore>(
self,
transparent_signing_set: &TransparentSigningSet,
sapling_extsks: &[sapling::zip32::ExtendedSpendingKey],
orchard_saks: &[orchard::keys::SpendAuthorizingKey],
rng: R,
) -> Result<BuildResult, Error<zip317::FeeError>> {
struct FakeCryptoRng<R: RngCore>(R);
impl<R: RngCore> CryptoRng for FakeCryptoRng<R> {}
impl<R: RngCore> RngCore for FakeCryptoRng<R> {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest)
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
self.0.try_fill_bytes(dest)
}
}
self.build(
transparent_signing_set,
sapling_extsks,
orchard_saks,
FakeCryptoRng(rng),
&MockSpendProver,
&MockOutputProver,
#[allow(deprecated)]
&zip317::FeeRule::standard(),
)
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "circuits")]
use {
super::{Builder, Error},
crate::transaction::builder::{BuildConfig, BundlePadding},
::sapling::{Node, Rseed, zip32::ExtendedSpendingKey},
::transparent::{address::TransparentAddress, builder::TransparentSigningSet},
assert_matches::assert_matches,
core::convert::Infallible,
ff::Field,
incrementalmerkletree::{frontier::CommitmentTree, witness::IncrementalWitness},
rand_core::OsRng,
zcash_protocol::{
consensus::{NetworkUpgrade, Parameters, TEST_NETWORK},
memo::MemoBytes,
value::{BalanceError, ZatBalance, Zatoshis},
},
};
#[cfg(feature = "transparent-inputs")]
use {
crate::transaction::{OutPoint, TxOut, TxVersion, builder::DEFAULT_TX_EXPIRY_DELTA},
::transparent::keys::{AccountPrivKey, IncomingViewingKey},
zcash_protocol::consensus::BranchId,
zip32::AccountId,
};
#[cfg(all(feature = "circuits", not(feature = "transparent-inputs")))]
use {crate::transaction::TxVersion, zcash_protocol::consensus::BranchId};
#[cfg(feature = "circuits")]
fn nu6_3_test_network() -> zcash_protocol::local_consensus::LocalNetwork {
use zcash_protocol::consensus::BlockHeight;
zcash_protocol::local_consensus::LocalNetwork {
overwinter: Some(BlockHeight::from_u32(1)),
sapling: Some(BlockHeight::from_u32(2)),
blossom: Some(BlockHeight::from_u32(3)),
heartwood: Some(BlockHeight::from_u32(4)),
canopy: Some(BlockHeight::from_u32(5)),
nu5: Some(BlockHeight::from_u32(6)),
nu6: Some(BlockHeight::from_u32(7)),
nu6_1: Some(BlockHeight::from_u32(8)),
nu6_2: Some(BlockHeight::from_u32(9)),
nu6_3: Some(BlockHeight::from_u32(10)),
#[cfg(zcash_unstable = "nu7")]
nu7: None,
}
}
#[cfg(all(feature = "circuits", zcash_unstable = "nu7"))]
fn nu7_test_network() -> zcash_protocol::local_consensus::LocalNetwork {
use zcash_protocol::consensus::BlockHeight;
zcash_protocol::local_consensus::LocalNetwork {
overwinter: Some(BlockHeight::from_u32(1)),
sapling: Some(BlockHeight::from_u32(2)),
blossom: Some(BlockHeight::from_u32(3)),
heartwood: Some(BlockHeight::from_u32(4)),
canopy: Some(BlockHeight::from_u32(5)),
nu5: Some(BlockHeight::from_u32(6)),
nu6: Some(BlockHeight::from_u32(7)),
nu6_1: Some(BlockHeight::from_u32(8)),
nu6_2: Some(BlockHeight::from_u32(9)),
nu6_3: Some(BlockHeight::from_u32(10)),
nu7: Some(BlockHeight::from_u32(11)),
}
}
#[test]
#[cfg(feature = "circuits")]
fn nu6_3_standard_builder_uses_v6_orchard_protocol() {
let builder = Builder::new(
nu6_3_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(10),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
assert_eq!(builder.tx_version, crate::transaction::TxVersion::V6);
assert_eq!(
builder.orchard_bundle_version,
Some(orchard::bundle::BundleVersion::orchard_v3())
);
}
#[test]
#[cfg(feature = "circuits")]
fn nu6_3_standard_builder_preserves_branch_orchard_protocol_for_explicit_v5() {
let mut builder = Builder::new(
nu6_3_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(10),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
builder
.propose_version::<Infallible>(crate::transaction::TxVersion::V5)
.unwrap();
assert_eq!(
builder.orchard_bundle_version,
Some(orchard::bundle::BundleVersion::orchard_v3())
);
}
#[test]
#[cfg(feature = "circuits")]
fn nu6_3_coinbase_builder_does_not_expose_orchard() {
let builder = Builder::new(
nu6_3_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(10),
BuildConfig::Coinbase { miner_data: None },
);
assert!(builder.orchard_builder.is_none());
}
#[test]
#[cfg(all(feature = "circuits", zcash_unstable = "nu7"))]
fn nu7_coinbase_builder_does_not_expose_orchard() {
let builder = Builder::new(
nu7_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(11),
BuildConfig::Coinbase { miner_data: None },
);
assert!(builder.orchard_builder.is_none());
}
#[test]
#[cfg(feature = "circuits")]
fn nu6_3_coinbase_builder_uses_ironwood_not_orchard() {
let builder = Builder::new(
nu6_3_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(10),
BuildConfig::Coinbase { miner_data: None },
);
assert!(builder.orchard_builder.is_none());
assert_eq!(
builder
.ironwood_builder
.as_ref()
.map(|_| orchard::bundle::BundleVersion::ironwood_v3()),
Some(orchard::bundle::BundleVersion::ironwood_v3())
);
}
#[test]
#[cfg(feature = "circuits")]
fn nu6_3_coinbase_builder_has_ironwood_output_option() {
let recipient = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(),
)
.address_at(0u32, orchard::keys::Scope::External);
let mut builder = Builder::new(
nu6_3_test_network(),
10u32.into(),
BuildConfig::Coinbase { miner_data: None },
);
assert_matches!(
builder.add_orchard_output::<Infallible>(
None,
recipient,
Zatoshis::const_from_u64(10_000),
MemoBytes::empty(),
),
Err(Error::OrchardBuilderNotAvailable)
);
builder
.add_ironwood_output::<Infallible>(
None,
recipient,
Zatoshis::const_from_u64(10_000),
MemoBytes::empty(),
)
.unwrap();
assert_eq!(
builder.ironwood_builder.as_ref().map(|b| b.outputs().len()),
Some(1)
);
assert_eq!(
super::orchard_action_count(
builder.ironwood_builder.as_ref().unwrap(),
true,
orchard::bundle::BundleVersion::ironwood_v3()
)
.unwrap(),
1
);
}
#[test]
#[cfg(all(feature = "circuits", feature = "transparent-inputs"))]
fn build_for_pczt_preserves_explicit_v6_without_ironwood() {
use ::transparent::keys::NonHardenedChildIndex;
let mut builder = Builder::new(
nu6_3_test_network(),
10u32.into(),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
builder
.propose_version::<Infallible>(TxVersion::V6)
.unwrap();
let mut transparent_signing_set = TransparentSigningSet::new();
let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::ZERO).unwrap();
let sk = tsk
.derive_external_secret_key(NonHardenedChildIndex::ZERO)
.unwrap();
let pubkey = transparent_signing_set.add_key(sk);
let prev_coin = TxOut::new(
Zatoshis::const_from_u64(50000),
tsk.to_account_pubkey()
.derive_external_ivk()
.unwrap()
.derive_address(NonHardenedChildIndex::ZERO)
.unwrap()
.script()
.into(),
);
builder
.add_transparent_p2pkh_input(pubkey, OutPoint::fake(), prev_coin)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(40000),
)
.unwrap();
let res = builder
.build_for_pczt(
OsRng,
&crate::transaction::fees::zip317::FeeRule::standard(),
)
.unwrap();
assert_eq!(res.pczt_parts.version, TxVersion::V6);
assert_eq!(
res.pczt_parts.consensus_branch_id,
zcash_protocol::consensus::BranchId::Nu6_3
);
}
#[test]
#[cfg(feature = "circuits")]
fn build_for_pczt_accepts_v6_when_ironwood_is_used() {
let mut builder = Builder::new(
nu6_3_test_network(),
10u32.into(),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
let recipient = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(),
)
.address_at(0u32, orchard::keys::Scope::External);
builder
.add_ironwood_output::<crate::transaction::fees::zip317::FeeRule>(
None,
recipient,
Zatoshis::const_from_u64(10_000),
MemoBytes::empty(),
)
.unwrap();
assert_matches!(
builder.build_for_pczt(
OsRng,
&crate::transaction::fees::zip317::FeeRule::standard(),
),
Err(Error::InsufficientFunds(_))
);
}
#[test]
#[cfg(feature = "circuits")]
fn build_for_pczt_rejects_explicit_v5_when_ironwood_is_used() {
let mut builder = Builder::new(
nu6_3_test_network(),
10u32.into(),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
builder
.propose_version::<Infallible>(TxVersion::V5)
.unwrap();
let recipient = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(),
)
.address_at(0u32, orchard::keys::Scope::External);
builder
.add_ironwood_output::<crate::transaction::fees::zip317::FeeRule>(
None,
recipient,
Zatoshis::const_from_u64(10_000),
MemoBytes::empty(),
)
.unwrap();
assert_matches!(
builder.build_for_pczt(
OsRng,
&crate::transaction::fees::zip317::FeeRule::standard(),
),
Err(Error::TargetIncompatible(
BranchId::Nu6_3,
TxVersion::V5,
None
))
);
}
#[cfg(feature = "circuits")]
fn ironwood_note_with_version(
version: orchard::note::NoteVersion,
) -> (
orchard::keys::FullViewingKey,
orchard::Note,
orchard::tree::MerklePath,
) {
let sk = orchard::keys::SpendingKey::from_bytes([7; 32]).unwrap();
let fvk = orchard::keys::FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32, orchard::keys::Scope::External);
let value = orchard::value::NoteValue::from_raw(99);
let rho = orchard::note::Rho::from_bytes(&[1; 32]).unwrap();
let rseed = (0u8..=255)
.find_map(|b| orchard::note::RandomSeed::from_bytes([b; 32], &rho).into_option())
.expect("at least one test rseed is valid");
let note = orchard::Note::from_parts(recipient, value, rho, rseed, version).unwrap();
let zero = orchard::tree::MerkleHashOrchard::from_bytes(&[0; 32]).unwrap();
let merkle_path = orchard::tree::MerklePath::from_parts(0, [zero; 32]);
(fvk, note, merkle_path)
}
#[test]
#[cfg(feature = "circuits")]
fn note_commitment_and_nullifier_depend_on_note_version() {
let (fvk, v2_note, _) = ironwood_note_with_version(orchard::note::NoteVersion::V2);
let (_, v3_note, _) = ironwood_note_with_version(orchard::note::NoteVersion::V3);
assert_ne!(
orchard::note::ExtractedNoteCommitment::from(v2_note.commitment()).to_bytes(),
orchard::note::ExtractedNoteCommitment::from(v3_note.commitment()).to_bytes(),
);
assert_ne!(
v2_note.nullifier(&fvk).to_bytes(),
v3_note.nullifier(&fvk).to_bytes(),
);
}
#[test]
#[cfg(feature = "circuits")]
fn add_ironwood_spend_rejects_v2_note_version() {
let mut builder = Builder::new(
nu6_3_test_network(),
10u32.into(),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
let (fvk, note, merkle_path) = ironwood_note_with_version(orchard::note::NoteVersion::V2);
assert_matches!(
builder.add_ironwood_spend::<Infallible>(fvk, note, merkle_path),
Err(Error::IronwoodSpendUnsupportedNoteVersion(
orchard::note::NoteVersion::V2
))
);
}
#[test]
#[cfg(feature = "circuits")]
fn orchard_action_count_uses_cross_address_disabled_count() {
let spend_sk = orchard::keys::SpendingKey::from_bytes([7; 32]).unwrap();
let spend_fvk = orchard::keys::FullViewingKey::from(&spend_sk);
let spend_recipient = spend_fvk.address_at(0u32, orchard::keys::Scope::External);
let rho = orchard::note::Rho::from_bytes(&[1; 32]).unwrap();
let rseed = (0u8..=255)
.find_map(|b| orchard::note::RandomSeed::from_bytes([b; 32], &rho).into_option())
.expect("at least one test rseed is valid");
let note = orchard::Note::from_parts(
spend_recipient,
orchard::value::NoteValue::from_raw(10_000),
rho,
rseed,
orchard::note::NoteVersion::V2,
)
.unwrap();
let leaf = orchard::tree::MerkleHashOrchard::from_cmx(¬e.commitment().into());
let mut tree = CommitmentTree::<orchard::tree::MerkleHashOrchard, 32>::empty();
tree.append(leaf).unwrap();
let witness = IncrementalWitness::from_tree(tree).unwrap();
let anchor = witness.root().into();
let merkle_path = witness.path().unwrap().into();
let mut builder = orchard::builder::Builder::new(
orchard::builder::BundleType::DEFAULT,
orchard::bundle::BundleVersion::orchard_v3(),
orchard::bundle::BundleVersion::orchard_v3().default_flags(),
anchor,
)
.unwrap();
builder.add_spend(spend_fvk, note, merkle_path).unwrap();
for seed in [[8u8; 32], [9u8; 32]] {
let change_fvk = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes(seed).unwrap(),
);
let recipient = change_fvk.address_at(0u32, orchard::keys::Scope::Internal);
builder
.add_change_output(
change_fvk,
None,
recipient,
orchard::value::NoteValue::from_raw(1_000),
[0u8; 512],
)
.unwrap();
}
assert_eq!(builder.spends().len(), 1);
assert_eq!(builder.changes().len(), 2);
assert_eq!(
super::orchard_action_count(
&builder,
false,
orchard::bundle::BundleVersion::orchard_v3(),
)
.unwrap(),
3
);
}
#[test]
#[cfg(feature = "circuits")]
fn orchard_padding_controls_padding() {
let recipient = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(),
)
.address_at(0u32, orchard::keys::Scope::External);
let config_with = |padding| BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
orchard_padding: padding,
ironwood_padding: BundlePadding::DEFAULT,
};
let count_for = |padding| {
let config = config_with(padding);
let mut builder = config
.orchard_builder(orchard::bundle::BundleVersion::orchard_v2())
.unwrap();
builder
.add_output(
None,
recipient,
orchard::value::NoteValue::from_raw(10_000),
[0u8; 512],
)
.unwrap();
super::orchard_action_count(
&builder,
false,
orchard::bundle::BundleVersion::orchard_v2(),
)
.unwrap()
};
assert_eq!(count_for(BundlePadding::DEFAULT), 2);
assert_eq!(count_for(BundlePadding::UNPADDED), 1);
}
#[test]
#[cfg(feature = "circuits")]
fn orchard_protocol_padding_is_per_pool() {
let bundle_types = |orchard_padding, ironwood_padding| {
let builder = Builder::new(
nu6_3_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(10),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
orchard_padding,
ironwood_padding,
},
);
(
builder.orchard_builder.as_ref().unwrap().bundle_type(),
builder.ironwood_builder.as_ref().unwrap().bundle_type(),
)
};
for (orchard_padding, ironwood_padding) in [
(BundlePadding::DEFAULT, BundlePadding::DEFAULT),
(BundlePadding::UNPADDED, BundlePadding::DEFAULT),
(BundlePadding::DEFAULT, BundlePadding::UNPADDED),
] {
assert_eq!(
bundle_types(orchard_padding, ironwood_padding),
(
orchard_padding.bundle_type(),
ironwood_padding.bundle_type()
),
);
}
}
#[test]
#[cfg(feature = "circuits")]
fn per_pool_bundle_types_build_two_plus_one_pczt() {
let spend_fvk = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes([7; 32]).unwrap(),
);
let recipient = spend_fvk.address_at(0u32, orchard::keys::Scope::External);
let rho = orchard::note::Rho::from_bytes(&[1; 32]).unwrap();
let rseed = (0u8..=255)
.find_map(|b| orchard::note::RandomSeed::from_bytes([b; 32], &rho).into_option())
.expect("at least one test rseed is valid");
let note = orchard::Note::from_parts(
recipient,
orchard::value::NoteValue::from_raw(100_000),
rho,
rseed,
orchard::note::NoteVersion::V2,
)
.unwrap();
let zero = orchard::tree::MerkleHashOrchard::from_bytes(&[0; 32]).unwrap();
let merkle_path = orchard::tree::MerklePath::from_parts(0, [zero; 32]);
let orchard_anchor = merkle_path.root(note.commitment().into());
let mut builder = Builder::new(
nu6_3_test_network(),
zcash_protocol::consensus::BlockHeight::from_u32(10),
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: Some(orchard_anchor),
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::UNPADDED,
},
);
builder
.add_orchard_spend::<crate::transaction::fees::zip317::FeeRule>(
spend_fvk,
note,
merkle_path,
)
.unwrap();
builder
.add_ironwood_output::<crate::transaction::fees::zip317::FeeRule>(
None,
recipient,
Zatoshis::const_from_u64(85_000),
MemoBytes::empty(),
)
.unwrap();
let result = builder
.build_for_pczt(
OsRng,
&crate::transaction::fees::zip317::FeeRule::standard(),
)
.unwrap();
assert_eq!(
result.pczt_parts.orchard.as_ref().unwrap().actions().len(),
2
);
assert_eq!(
result.pczt_parts.ironwood.as_ref().unwrap().actions().len(),
1
);
}
#[test]
#[cfg(feature = "circuits")]
fn add_orchard_change_output_records_change() {
let target_height = TEST_NETWORK.activation_height(NetworkUpgrade::Nu5).unwrap();
let mut builder = Builder::new(
TEST_NETWORK,
target_height,
BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
);
let fvk = orchard::keys::FullViewingKey::from(
&orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(),
);
let recipient = fvk.address_at(0u32, orchard::keys::Scope::Internal);
builder
.add_orchard_change_output::<Infallible>(
fvk,
None,
recipient,
Zatoshis::const_from_u64(5_000),
MemoBytes::empty(),
)
.unwrap();
assert_eq!(
builder.orchard_builder.as_ref().map(|b| b.changes().len()),
Some(1)
);
}
#[test]
#[cfg(feature = "transparent-inputs")]
fn binding_sig_absent_if_no_shielded_spend_or_output() {
use crate::transaction::builder::{self, TransparentBuilder};
use ::transparent::{builder::TransparentSigningSet, keys::NonHardenedChildIndex};
use zcash_protocol::consensus::NetworkUpgrade;
let sapling_activation_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap();
let consensus_branch_id = BranchId::for_height(&TEST_NETWORK, sapling_activation_height);
let mut builder = builder::Builder {
params: TEST_NETWORK,
tx_version: TxVersion::suggested_for_branch(consensus_branch_id),
consensus_branch_id,
build_config: BuildConfig::Standard {
sapling_anchor: Some(sapling::Anchor::empty_tree()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
},
target_height: sapling_activation_height,
expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
zip233_amount: Zatoshis::ZERO,
transparent_builder: TransparentBuilder::empty(),
sapling_builder: None,
orchard_builder: None,
orchard_bundle_version: None,
ironwood_builder: None,
_progress_notifier: (),
};
let mut transparent_signing_set = TransparentSigningSet::new();
let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::ZERO).unwrap();
let sk = tsk
.derive_external_secret_key(NonHardenedChildIndex::ZERO)
.unwrap();
let pubkey = transparent_signing_set.add_key(sk);
let prev_coin = TxOut::new(
Zatoshis::const_from_u64(50000),
tsk.to_account_pubkey()
.derive_external_ivk()
.unwrap()
.derive_address(NonHardenedChildIndex::ZERO)
.unwrap()
.script()
.into(),
);
builder
.add_transparent_p2pkh_input(pubkey, OutPoint::fake(), prev_coin)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(40000),
)
.unwrap();
let res = builder
.mock_build(&transparent_signing_set, &[], &[], OsRng)
.unwrap();
assert!(res.transaction().sapling_bundle.is_none());
}
#[test]
#[cfg(all(feature = "circuits", feature = "transparent-inputs"))]
fn build_uses_overridden_expiry_height() {
use ::transparent::keys::NonHardenedChildIndex;
let tx_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap();
let build_config = BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder =
Builder::new(TEST_NETWORK, tx_height, build_config).with_expiry_height(0u32.into());
let mut transparent_signing_set = TransparentSigningSet::new();
let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::ZERO).unwrap();
let sk = tsk
.derive_external_secret_key(NonHardenedChildIndex::ZERO)
.unwrap();
let pubkey = transparent_signing_set.add_key(sk);
let prev_coin = TxOut::new(
Zatoshis::const_from_u64(50_000),
tsk.to_account_pubkey()
.derive_external_ivk()
.unwrap()
.derive_address(NonHardenedChildIndex::ZERO)
.unwrap()
.script()
.into(),
);
builder
.add_transparent_p2pkh_input(pubkey, OutPoint::fake(), prev_coin)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(40_000),
)
.unwrap();
let res = builder
.mock_build(&transparent_signing_set, &[], &[], OsRng)
.unwrap();
assert_eq!(res.transaction().expiry_height(), 0u32.into());
}
#[test]
#[cfg(feature = "circuits")]
fn build_rejects_mismatched_coinbase_expiry_height() {
let tx_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap();
let build_config = BuildConfig::Coinbase { miner_data: None };
let mut builder =
Builder::new(TEST_NETWORK, tx_height, build_config).with_expiry_height(0u32.into());
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(50_000),
)
.unwrap();
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), &[], &[], OsRng),
Err(Error::CoinbaseExpiryHeightMismatch {
target_height,
expiry_height,
}) if target_height == tx_height && expiry_height == 0u32.into()
);
}
#[test]
#[cfg(feature = "circuits")]
fn binding_sig_present_if_shielded_spend() {
let extsk = ExtendedSpendingKey::master(&[]);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let to = dfvk.default_address().1;
let mut rng = OsRng;
let note1 = to.create_note(
sapling::value::NoteValue::from_raw(50000),
Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)),
);
let cmu1 = Node::from_cmu(¬e1.cmu());
let mut tree = CommitmentTree::<Node, 32>::empty();
tree.append(cmu1).unwrap();
let witness1 = IncrementalWitness::from_tree(tree).unwrap();
let tx_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap();
let build_config = BuildConfig::Standard {
sapling_anchor: Some(witness1.root().into()),
orchard_anchor: None,
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_sapling_spend::<Infallible>(dfvk.fvk().clone(), note1, witness1.path().unwrap())
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(35000),
)
.unwrap();
let res = builder
.mock_build(&TransparentSigningSet::new(), &[extsk], &[], OsRng)
.unwrap();
assert!(res.transaction().sapling_bundle().is_some());
}
#[test]
#[cfg(feature = "circuits")]
fn fails_on_negative_change() {
use crate::transaction::fees::zip317::MINIMUM_FEE;
let mut rng = OsRng;
let extsk = ExtendedSpendingKey::master(&[]);
let tx_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap();
{
let build_config = BuildConfig::Standard {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let builder = Builder::new(TEST_NETWORK, tx_height, build_config);
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), &[], &[], OsRng),
Err(Error::InsufficientFunds(expected)) if expected == MINIMUM_FEE.into()
);
}
let dfvk = extsk.to_diversifiable_full_viewing_key();
let ovk = Some(dfvk.fvk().ovk);
let to = dfvk.default_address().1;
let extsks = &[extsk];
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(sapling::Anchor::empty_tree()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_sapling_output::<Infallible>(
ovk,
to,
Zatoshis::const_from_u64(50000),
MemoBytes::empty(),
)
.unwrap();
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng),
Err(Error::InsufficientFunds(expected)) if
expected == (Zatoshis::const_from_u64(50000) + MINIMUM_FEE).unwrap().into()
);
}
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(sapling::Anchor::empty_tree()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(50000),
)
.unwrap();
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng),
Err(Error::InsufficientFunds(expected)) if expected ==
(Zatoshis::const_from_u64(50000) + MINIMUM_FEE).unwrap().into()
);
}
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(sapling::Anchor::empty_tree()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder.set_zip233_amount(Zatoshis::const_from_u64(50000));
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng),
Err(Error::InsufficientFunds(expected)) if expected ==
(Zatoshis::const_from_u64(50000) + MINIMUM_FEE).unwrap().into()
);
}
let note1 = to.create_note(
sapling::value::NoteValue::from_raw(59999),
Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)),
);
let cmu1 = Node::from_cmu(¬e1.cmu());
let mut tree = CommitmentTree::<Node, 32>::empty();
tree.append(cmu1).unwrap();
let mut witness1 = IncrementalWitness::from_tree(tree.clone()).unwrap();
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(witness1.root().into()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_sapling_spend::<Infallible>(
dfvk.fvk().clone(),
note1.clone(),
witness1.path().unwrap(),
)
.unwrap();
builder
.add_sapling_output::<Infallible>(
ovk,
to,
Zatoshis::const_from_u64(30000),
MemoBytes::empty(),
)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(15000),
)
.unwrap();
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng),
Err(Error::InsufficientFunds(expected)) if expected == ZatBalance::const_from_i64(1)
);
}
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(witness1.root().into()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_sapling_spend::<Infallible>(
dfvk.fvk().clone(),
note1.clone(),
witness1.path().unwrap(),
)
.unwrap();
builder
.add_sapling_output::<Infallible>(
ovk,
to,
Zatoshis::const_from_u64(30000),
MemoBytes::empty(),
)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(5000),
)
.unwrap();
builder.set_zip233_amount(Zatoshis::const_from_u64(10000));
assert_matches!(
builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng),
Err(Error::InsufficientFunds(expected)) if expected == ZatBalance::const_from_i64(1)
);
}
let note2 = to.create_note(
sapling::value::NoteValue::from_raw(1),
Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)),
);
let cmu2 = Node::from_cmu(¬e2.cmu());
tree.append(cmu2).unwrap();
witness1.append(cmu2).unwrap();
let witness2 = IncrementalWitness::from_tree(tree).unwrap();
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(witness1.root().into()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_sapling_spend::<Infallible>(
dfvk.fvk().clone(),
note1.clone(),
witness1.path().unwrap(),
)
.unwrap();
builder
.add_sapling_spend::<Infallible>(
dfvk.fvk().clone(),
note2.clone(),
witness2.path().unwrap(),
)
.unwrap();
builder
.add_sapling_output::<Infallible>(
ovk,
to,
Zatoshis::const_from_u64(30000),
MemoBytes::empty(),
)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(15000),
)
.unwrap();
let res = builder
.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng)
.unwrap();
assert_eq!(
res.transaction()
.fee_paid(|_| Err(BalanceError::Overflow))
.unwrap(),
Some(Zatoshis::const_from_u64(15_000))
);
}
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
{
let build_config = BuildConfig::Standard {
sapling_anchor: Some(witness1.root().into()),
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
orchard_padding: BundlePadding::DEFAULT,
ironwood_padding: BundlePadding::DEFAULT,
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder
.add_sapling_spend::<Infallible>(
dfvk.fvk().clone(),
note1,
witness1.path().unwrap(),
)
.unwrap();
builder
.add_sapling_spend::<Infallible>(
dfvk.fvk().clone(),
note2,
witness2.path().unwrap(),
)
.unwrap();
builder
.add_sapling_output::<Infallible>(
ovk,
to,
Zatoshis::const_from_u64(30000),
MemoBytes::empty(),
)
.unwrap();
builder
.add_transparent_output(
&TransparentAddress::PublicKeyHash([0; 20]),
Zatoshis::const_from_u64(5000),
)
.unwrap();
builder.set_zip233_amount(Zatoshis::const_from_u64(10000));
let res = builder
.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng)
.unwrap();
assert_eq!(
res.transaction()
.fee_paid(|_| Err(BalanceError::Overflow))
.unwrap(),
Some(Zatoshis::const_from_u64(15_000))
);
}
}
#[cfg(all(feature = "circuits", feature = "std"))]
#[test]
fn cached_orchard_proving_key_reuses_one_instance_per_version() {
use core::ptr;
use orchard::circuit::OrchardCircuitVersion;
use super::cached_orchard_proving_key;
let first = cached_orchard_proving_key(OrchardCircuitVersion::FixedPostNu6_2);
let second = cached_orchard_proving_key(OrchardCircuitVersion::FixedPostNu6_2);
assert!(ptr::eq(first, second));
let other = cached_orchard_proving_key(OrchardCircuitVersion::PostNu6_3);
assert!(!ptr::eq(first, other));
}
}