Merge pull request #1065 from nuttycom/primitives/padding_rule

zcash_primitives: Add explicit control over padding to the transaction builder.
This commit is contained in:
Kris Nuttycombe 2024-01-02 15:03:55 -07:00 committed by GitHub
commit 6c5bdf85ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 535 additions and 296 deletions

6
Cargo.lock generated
View File

@ -1461,8 +1461,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "orchard" name = "orchard"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/orchard.git?rev=189257391a5726d7f3f2568d547a5ef8603c7156#189257391a5726d7f3f2568d547a5ef8603c7156"
checksum = "5d31e68534df32024dcc89a8390ec6d7bef65edd87d91b45cfb481a2eb2d77c5"
dependencies = [ dependencies = [
"aes", "aes",
"bitvec", "bitvec",
@ -2088,7 +2087,7 @@ dependencies = [
[[package]] [[package]]
name = "sapling-crypto" name = "sapling-crypto"
version = "0.0.1" version = "0.0.1"
source = "git+https://github.com/zcash/sapling-crypto.git?rev=df6681c1046bd28073befd7d42224e4a4c70032a#df6681c1046bd28073befd7d42224e4a4c70032a" source = "git+https://github.com/zcash/sapling-crypto.git?rev=71711b9e4b775a625318070e0f56b7b652f12306#71711b9e4b775a625318070e0f56b7b652f12306"
dependencies = [ dependencies = [
"aes", "aes",
"bellman", "bellman",
@ -3076,6 +3075,7 @@ dependencies = [
"blake2b_simd", "blake2b_simd",
"ff", "ff",
"jubjub", "jubjub",
"orchard",
"rand_core", "rand_core",
"sapling-crypto", "sapling-crypto",
"zcash_address", "zcash_address",

View File

@ -112,4 +112,5 @@ panic = 'abort'
codegen-units = 1 codegen-units = 1
[patch.crates-io] [patch.crates-io]
sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "df6681c1046bd28073befd7d42224e4a4c70032a" } sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "71711b9e4b775a625318070e0f56b7b652f12306" }
orchard = { git = "https://github.com/zcash/orchard.git", rev = "189257391a5726d7f3f2568d547a5ef8603c7156" }

View File

@ -32,6 +32,9 @@ zcash_primitives.workspace = true
time = "0.3.22" time = "0.3.22"
nonempty.workspace = true nonempty.workspace = true
# - CSPRNG
rand_core.workspace = true
# - Encodings # - Encodings
base64.workspace = true base64.workspace = true
bech32.workspace = true bech32.workspace = true

View File

@ -1,5 +1,6 @@
use std::num::NonZeroU32; use std::num::NonZeroU32;
use rand_core::OsRng;
use sapling::{ use sapling::{
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
prover::{OutputProver, SpendProver}, prover::{OutputProver, SpendProver},
@ -8,7 +9,7 @@ use zcash_primitives::{
consensus::{self, NetworkUpgrade}, consensus::{self, NetworkUpgrade},
memo::MemoBytes, memo::MemoBytes,
transaction::{ transaction::{
builder::Builder, builder::{BuildConfig, Builder},
components::amount::{Amount, NonNegativeAmount}, components::amount::{Amount, NonNegativeAmount},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
Transaction, TxId, Transaction, TxId,
@ -576,36 +577,59 @@ where
Some(dfvk.to_ovk(Scope::Internal)) Some(dfvk.to_ovk(Scope::Internal))
}; };
let (sapling_anchor, sapling_inputs) = proposal.sapling_inputs().map_or_else(
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
let anchor = sapling_tree
.root_at_checkpoint_id(&inputs.anchor_height())?
.into();
let sapling_inputs = inputs
.notes()
.iter()
.map(|selected| {
match selected.note() {
WalletNote::Sapling(note) => {
let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let merkle_path = sapling_tree.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&inputs.anchor_height(),
)?;
Ok((key, note, merkle_path))
}
WalletNote::Orchard(_) => {
// FIXME: Implement this once `Proposal` has been refactored to
// include Orchard notes.
panic!("Orchard spends are not yet supported");
}
}
})
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
Ok((anchor, sapling_inputs))
})
},
)?;
// Create the transaction. The type of the proposal ensures that there // Create the transaction. The type of the proposal ensures that there
// are no possible transparent inputs, so we ignore those // are no possible transparent inputs, so we ignore those
let mut builder = Builder::new(params.clone(), proposal.min_target_height(), None); let mut builder = Builder::new(
params.clone(),
proposal.min_target_height(),
BuildConfig::Standard {
sapling_anchor,
orchard_anchor: orchard::Anchor::empty_tree(),
},
);
if let Some(sapling_inputs) = proposal.sapling_inputs() { for (key, note, merkle_path) in sapling_inputs.into_iter() {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| { builder.add_sapling_spend(&key, note.clone(), merkle_path)?;
for selected in sapling_inputs.notes() {
match selected.note() {
WalletNote::Sapling(note) => {
let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let merkle_path = sapling_tree.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&sapling_inputs.anchor_height(),
)?;
builder.add_sapling_spend(key, note.clone(), merkle_path)?;
}
WalletNote::Orchard(_) => {
// FIXME: Implement this once `Proposal` has been refactored to
// include Orchard notes.
panic!("Orchard spends are not yet supported");
}
}
}
Ok(())
})?;
} }
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -720,7 +744,7 @@ where
// Build the transaction with the specified fee rule // Build the transaction with the specified fee rule
let (tx, sapling_build_meta) = let (tx, sapling_build_meta) =
builder.build(spend_prover, output_prover, proposal.fee_rule())?; builder.build(OsRng, spend_prover, output_prover, proposal.fee_rule())?;
let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
let sapling_outputs = let sapling_outputs =

View File

@ -12,7 +12,7 @@ use zcash_primitives::{
}, },
}; };
pub mod common; pub(crate) mod common;
pub mod fixed; pub mod fixed;
pub mod sapling; pub mod sapling;
pub mod standard; pub mod standard;

View File

@ -3,7 +3,7 @@
use std::convert::Infallible; use std::convert::Infallible;
use sapling::builder::{SaplingOutputInfo, SpendDescriptionInfo}; use sapling::builder::{OutputInfo, SpendInfo};
use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::components::amount::NonNegativeAmount;
/// A trait that provides a minimized view of a Sapling input suitable for use in /// A trait that provides a minimized view of a Sapling input suitable for use in
@ -26,7 +26,7 @@ impl<N> InputView<N> for Infallible {
// `SpendDescriptionInfo` does not contain a note identifier, so we can only implement // `SpendDescriptionInfo` does not contain a note identifier, so we can only implement
// `InputView<()>` // `InputView<()>`
impl InputView<()> for SpendDescriptionInfo { impl InputView<()> for SpendInfo {
fn note_id(&self) -> &() { fn note_id(&self) -> &() {
&() &()
} }
@ -44,7 +44,7 @@ pub trait OutputView {
fn value(&self) -> NonNegativeAmount; fn value(&self) -> NonNegativeAmount;
} }
impl OutputView for SaplingOutputInfo { impl OutputView for OutputInfo {
fn value(&self) -> NonNegativeAmount { fn value(&self) -> NonNegativeAmount {
NonNegativeAmount::try_from(self.value()) NonNegativeAmount::try_from(self.value())
.expect("Output values should be checked at construction.") .expect("Output values should be checked at construction.")

View File

@ -19,6 +19,7 @@ ff.workspace = true
jubjub.workspace = true jubjub.workspace = true
rand_core.workspace = true rand_core.workspace = true
sapling.workspace = true sapling.workspace = true
orchard.workspace = true
zcash_address.workspace = true zcash_address.workspace = true
zcash_proofs.workspace = true zcash_proofs.workspace = true

View File

@ -476,6 +476,8 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<B> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::convert::Infallible;
use blake2b_simd::Params; use blake2b_simd::Params;
use ff::Field; use ff::Field;
use rand_core::OsRng; use rand_core::OsRng;
@ -487,7 +489,7 @@ mod tests {
extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload},
legacy::TransparentAddress, legacy::TransparentAddress,
transaction::{ transaction::{
builder::Builder, builder::{BuildConfig, Builder},
components::{ components::{
amount::{Amount, NonNegativeAmount}, amount::{Amount, NonNegativeAmount},
tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut}, tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut},
@ -637,9 +639,19 @@ mod tests {
} }
} }
fn demo_builder<'a>(height: BlockHeight) -> DemoBuilder<Builder<'a, FutureNetwork, OsRng, ()>> { fn demo_builder<'a>(
height: BlockHeight,
sapling_anchor: sapling::Anchor,
) -> DemoBuilder<Builder<'a, FutureNetwork, ()>> {
DemoBuilder { DemoBuilder {
txn_builder: Builder::new(FutureNetwork, height, None), txn_builder: Builder::new(
FutureNetwork,
height,
BuildConfig::Standard {
sapling_anchor,
orchard_anchor: orchard::Anchor::empty_tree(),
},
),
extension_id: 0, extension_id: 0,
} }
} }
@ -823,9 +835,9 @@ mod tests {
tree.append(cm1).unwrap(); tree.append(cm1).unwrap();
let witness1 = sapling::IncrementalWitness::from_tree(tree); let witness1 = sapling::IncrementalWitness::from_tree(tree);
let mut builder_a = demo_builder(tx_height); let mut builder_a = demo_builder(tx_height, witness1.root().into());
builder_a builder_a
.add_sapling_spend(extsk, note1, witness1.path().unwrap()) .add_sapling_spend::<Infallible>(&extsk, note1, witness1.path().unwrap())
.unwrap(); .unwrap();
let value = NonNegativeAmount::const_from_u64(100000); let value = NonNegativeAmount::const_from_u64(100000);
@ -836,7 +848,7 @@ mod tests {
.unwrap(); .unwrap();
let (tx_a, _) = builder_a let (tx_a, _) = builder_a
.txn_builder .txn_builder
.build_zfuture(&prover, &prover, &fee_rule) .build_zfuture(OsRng, &prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_a = tx_a.tze_bundle().unwrap(); let tze_a = tx_a.tze_bundle().unwrap();
@ -845,7 +857,7 @@ mod tests {
// Transfer // Transfer
// //
let mut builder_b = demo_builder(tx_height + 1); let mut builder_b = demo_builder(tx_height + 1, sapling::Anchor::empty_tree());
let prevout_a = (OutPoint::new(tx_a.txid(), 0), tze_a.vout[0].clone()); let prevout_a = (OutPoint::new(tx_a.txid(), 0), tze_a.vout[0].clone());
let value_xfr = (value - fee_rule.fixed_fee()).unwrap(); let value_xfr = (value - fee_rule.fixed_fee()).unwrap();
builder_b builder_b
@ -854,7 +866,7 @@ mod tests {
.unwrap(); .unwrap();
let (tx_b, _) = builder_b let (tx_b, _) = builder_b
.txn_builder .txn_builder
.build_zfuture(&prover, &prover, &fee_rule) .build_zfuture(OsRng, &prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_b = tx_b.tze_bundle().unwrap(); let tze_b = tx_b.tze_bundle().unwrap();
@ -863,7 +875,7 @@ mod tests {
// Closing transaction // Closing transaction
// //
let mut builder_c = demo_builder(tx_height + 2); let mut builder_c = demo_builder(tx_height + 2, sapling::Anchor::empty_tree());
let prevout_b = (OutPoint::new(tx_a.txid(), 0), tze_b.vout[0].clone()); let prevout_b = (OutPoint::new(tx_a.txid(), 0), tze_b.vout[0].clone());
builder_c builder_c
.demo_close(prevout_b, preimage_2) .demo_close(prevout_b, preimage_2)
@ -879,7 +891,7 @@ mod tests {
let (tx_c, _) = builder_c let (tx_c, _) = builder_c
.txn_builder .txn_builder
.build_zfuture(&prover, &prover, &fee_rule) .build_zfuture(OsRng, &prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_c = tx_c.tze_bundle().unwrap(); let tze_c = tx_c.tze_bundle().unwrap();

View File

@ -10,7 +10,8 @@ and this library adheres to Rust's notion of
- Dependency on `bellman 0.14`. - Dependency on `bellman 0.14`.
- `zcash_primitives::consensus::sapling_zip212_enforcement` - `zcash_primitives::consensus::sapling_zip212_enforcement`
- `zcash_primitives::transaction`: - `zcash_primitives::transaction`:
- `builder::get_fee` - `builder::{BuildConfig, FeeError, get_fee}`
- `builder::Error::SaplingBuilderNotAvailable`
- `components::sapling`: - `components::sapling`:
- Sapling bundle component parsers, behind the `temporary-zcashd` feature - Sapling bundle component parsers, behind the `temporary-zcashd` feature
flag: flag:
@ -55,14 +56,31 @@ and this library adheres to Rust's notion of
- `builder::Builder` now has a generic parameter for the type of progress - `builder::Builder` now has a generic parameter for the type of progress
notifier, which needs to implement `sapling::builder::ProverProgress` in notifier, which needs to implement `sapling::builder::ProverProgress` in
order to build transactions. order to build transactions.
- `builder::Builder::new` now takes a `BuildConfig` argument instead of an
optional Orchard anchor. Anchors for both Sapling and Orchard are now
required at the time of builder construction.
- `builder::Builder::{build, build_zfuture}` now take - `builder::Builder::{build, build_zfuture}` now take
`&impl SpendProver, &impl OutputProver` instead of `&impl TxProver`. `&impl SpendProver, &impl OutputProver` instead of `&impl TxProver`.
- `builder::Builder::add_sapling_spend` no longer takes a `diversifier` - `builder::Builder::add_sapling_spend` no longer takes a `diversifier`
argument as the diversifier may be obtained from the note. argument as the diversifier may be obtained from the note.
- `builder::Builder::add_sapling_spend` now takes its `ExtendedSpendingKey`
argument by reference.
- `builder::Builder::{add_sapling_spend, add_sapling_output}` now return
`builder::Error`s instead of the underlying `sapling_crypto::builder::Error`s
when returning `Err`.
- `builder::Builder::add_orchard_spend` now takes its `SpendingKey` argument
by reference.
- `builder::Builder::with_progress_notifier` now consumes `self` and returns a - `builder::Builder::with_progress_notifier` now consumes `self` and returns a
`Builder` typed on the provided channel. `Builder` typed on the provided channel.
- `builder::Builder::get_fee` now returns a `builder::FeeError` instead of the
bare `FeeRule::Error` when returning `Err`.
- `builder::Error::OrchardAnchorNotAvailable` has been renamed to
`OrchardBuilderNotAvailable`.
- `builder::{build, build_zfuture}` each now take an additional `rng` argument.
- `components::transparent::TxOut.value` now has type `NonNegativeAmount` - `components::transparent::TxOut.value` now has type `NonNegativeAmount`
instead of `Amount`. instead of `Amount`.
- `components::sapling::MapAuth` trait methods now take `&mut self` instead
of `&self`.
- `components::transparent::fees` has been moved to - `components::transparent::fees` has been moved to
`zcash_primitives::transaction::fees::transparent` `zcash_primitives::transaction::fees::transparent`
- `components::transparent::builder::TransparentBuilder::{inputs, outputs}` - `components::transparent::builder::TransparentBuilder::{inputs, outputs}`
@ -102,7 +120,7 @@ and this library adheres to Rust's notion of
- `Bundle` - `Bundle`
- `SpendDescription, SpendDescriptionV5` - `SpendDescription, SpendDescriptionV5`
- `OutputDescription, OutputDescriptionV5` - `OutputDescription, OutputDescriptionV5`
- `Authorization, Authorized, MapAuth` - `Authorization, Authorized`
- `GrothProofBytes` - `GrothProofBytes`
- `CompactOutputDescription` (moved to `sapling_crypto::note_encryption`). - `CompactOutputDescription` (moved to `sapling_crypto::note_encryption`).
- `Unproven` - `Unproven`

View File

@ -91,6 +91,7 @@ incrementalmerkletree = { workspace = true, features = ["legacy-api", "test-depe
proptest.workspace = true proptest.workspace = true
assert_matches.workspace = true assert_matches.workspace = true
rand_xorshift.workspace = true rand_xorshift.workspace = true
sapling = { workspace = true, features = ["test-dependencies"] }
orchard = { workspace = true, features = ["test-dependencies"] } orchard = { workspace = true, features = ["test-dependencies"] }
[target.'cfg(unix)'.dev-dependencies] [target.'cfg(unix)'.dev-dependencies]

View File

@ -4,7 +4,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Through
use ff::Field; use ff::Field;
use rand_core::OsRng; use rand_core::OsRng;
use sapling::{ use sapling::{
builder::SaplingBuilder, self,
note_encryption::{ note_encryption::{
try_sapling_compact_note_decryption, try_sapling_note_decryption, CompactOutputDescription, try_sapling_compact_note_decryption, try_sapling_note_decryption, CompactOutputDescription,
PreparedIncomingViewingKey, SaplingDomain, PreparedIncomingViewingKey, SaplingDomain,
@ -35,9 +35,15 @@ fn bench_note_decryption(c: &mut Criterion) {
let diversifier = Diversifier([0; 11]); let diversifier = Diversifier([0; 11]);
let pa = valid_ivk.to_payment_address(diversifier).unwrap(); let pa = valid_ivk.to_payment_address(diversifier).unwrap();
let mut builder = SaplingBuilder::new(zip212_enforcement); let mut builder = sapling::builder::Builder::new(
zip212_enforcement,
// We use the Coinbase bundle type because we don't need to use
// any inputs for this benchmark.
sapling::builder::BundleType::Coinbase,
sapling::Anchor::empty_tree(),
);
builder builder
.add_output(&mut rng, None, pa, NoteValue::from_raw(100), None) .add_output(None, pa, NoteValue::from_raw(100), None)
.unwrap(); .unwrap();
let (bundle, _) = builder let (bundle, _) = builder
.build::<MockSpendProver, MockOutputProver, _, Amount>(&mut rng) .build::<MockSpendProver, MockOutputProver, _, Amount>(&mut rng)

View File

@ -5,7 +5,7 @@ use std::error;
use std::fmt; use std::fmt;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use rand::{rngs::OsRng, CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use crate::{ use crate::{
consensus::{self, BlockHeight, BranchId, NetworkUpgrade}, consensus::{self, BlockHeight, BranchId, NetworkUpgrade},
@ -13,7 +13,7 @@ use crate::{
memo::MemoBytes, memo::MemoBytes,
sapling::{ sapling::{
self, self,
builder::{self as sapling_builder, SaplingBuilder, SaplingMetadata}, builder::SaplingMetadata,
prover::{OutputProver, SpendProver}, prover::{OutputProver, SpendProver},
Note, PaymentAddress, Note, PaymentAddress,
}, },
@ -53,9 +53,25 @@ use super::components::amount::NonNegativeAmount;
/// <https://zips.z.cash/zip-0203#changes-for-blossom> /// <https://zips.z.cash/zip-0203#changes-for-blossom>
const DEFAULT_TX_EXPIRY_DELTA: u32 = 40; const DEFAULT_TX_EXPIRY_DELTA: u32 = 40;
/// Errors that can occur during fee calculation.
#[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),
}
}
}
/// Errors that can occur during transaction construction. /// Errors that can occur during transaction construction.
#[derive(Debug)] #[derive(Debug)]
pub enum Error<FeeError> { pub enum Error<FE> {
/// Insufficient funds were provided to the transaction builder; the given /// Insufficient funds were provided to the transaction builder; the given
/// additional amount is required in order to construct the transaction. /// additional amount is required in order to construct the transaction.
InsufficientFunds(Amount), InsufficientFunds(Amount),
@ -63,22 +79,25 @@ pub enum Error<FeeError> {
/// add a change output. /// add a change output.
ChangeRequired(Amount), ChangeRequired(Amount),
/// An error occurred in computing the fees for a transaction. /// An error occurred in computing the fees for a transaction.
Fee(FeeError), Fee(FeeError<FE>),
/// An overflow or underflow occurred when computing value balances /// An overflow or underflow occurred when computing value balances
Balance(BalanceError), Balance(BalanceError),
/// An error occurred in constructing the transparent parts of a transaction. /// An error occurred in constructing the transparent parts of a transaction.
TransparentBuild(transparent::builder::Error), TransparentBuild(transparent::builder::Error),
/// An error occurred in constructing the Sapling parts of a transaction. /// An error occurred in constructing the Sapling parts of a transaction.
SaplingBuild(sapling_builder::Error), SaplingBuild(sapling::builder::Error),
/// An error occurred in constructing the Orchard parts of a transaction. /// An error occurred in constructing the Orchard parts of a transaction.
OrchardBuild(orchard::builder::BuildError), OrchardBuild(orchard::builder::BuildError),
/// An error occurred in adding an Orchard Spend to a transaction. /// An error occurred in adding an Orchard Spend to a transaction.
OrchardSpend(orchard::builder::SpendError), OrchardSpend(orchard::builder::SpendError),
/// An error occurred in adding an Orchard Output to a transaction. /// An error occurred in adding an Orchard Output to a transaction.
OrchardRecipient(orchard::builder::OutputError), OrchardRecipient(orchard::builder::OutputError),
/// The builder was constructed either without an Orchard anchor or before NU5 /// The builder was constructed without support for the Sapling pool, but a Sapling
/// activation, but an Orchard spend or recipient was added. /// spend or output was added.
OrchardAnchorNotAvailable, SaplingBuilderNotAvailable,
/// The builder was constructed with a target height before NU5 activation, but an Orchard
/// spend or output was added.
OrchardBuilderNotAvailable,
/// An error occurred in constructing the TZE parts of a transaction. /// An error occurred in constructing the TZE parts of a transaction.
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
TzeBuild(tze::builder::Error), TzeBuild(tze::builder::Error),
@ -104,7 +123,11 @@ impl<FE: fmt::Display> fmt::Display for Error<FE> {
Error::OrchardBuild(err) => write!(f, "{:?}", err), Error::OrchardBuild(err) => write!(f, "{:?}", err),
Error::OrchardSpend(err) => write!(f, "Could not add Orchard spend: {}", err), Error::OrchardSpend(err) => write!(f, "Could not add Orchard spend: {}", err),
Error::OrchardRecipient(err) => write!(f, "Could not add Orchard recipient: {}", err), Error::OrchardRecipient(err) => write!(f, "Could not add Orchard recipient: {}", err),
Error::OrchardAnchorNotAvailable => write!( Error::SaplingBuilderNotAvailable => write!(
f,
"Cannot create Sapling transactions without a Sapling anchor"
),
Error::OrchardBuilderNotAvailable => write!(
f, f,
"Cannot create Orchard transactions without an Orchard anchor, or before NU5 activation" "Cannot create Orchard transactions without an Orchard anchor, or before NU5 activation"
), ),
@ -122,6 +145,24 @@ impl<FE> From<BalanceError> for Error<FE> {
} }
} }
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)
}
}
/// Reports on the progress made by the builder towards building a transaction. /// Reports on the progress made by the builder towards building a transaction.
pub struct Progress { pub struct Progress {
/// The number of steps completed. /// The number of steps completed.
@ -156,14 +197,52 @@ impl Progress {
} }
} }
/// Rules for how the builder should be configured for each shielded pool.
#[derive(Clone, Copy)]
pub enum BuildConfig {
Standard {
sapling_anchor: sapling::Anchor,
orchard_anchor: orchard::Anchor,
},
Coinbase,
}
impl BuildConfig {
/// Returns the Sapling bundle type and anchor for this configuration.
pub fn sapling_builder_config(&self) -> (sapling::builder::BundleType, sapling::Anchor) {
match self {
BuildConfig::Standard { sapling_anchor, .. } => {
(sapling::builder::BundleType::DEFAULT, *sapling_anchor)
}
BuildConfig::Coinbase => (
sapling::builder::BundleType::Coinbase,
sapling::Anchor::empty_tree(),
),
}
}
/// Returns the Orchard bundle type and anchor for this configuration.
pub fn orchard_builder_config(&self) -> (orchard::builder::BundleType, orchard::Anchor) {
match self {
BuildConfig::Standard { orchard_anchor, .. } => {
(orchard::builder::BundleType::DEFAULT, *orchard_anchor)
}
BuildConfig::Coinbase => (
orchard::builder::BundleType::Coinbase,
orchard::Anchor::empty_tree(),
),
}
}
}
/// Generates a [`Transaction`] from its inputs and outputs. /// Generates a [`Transaction`] from its inputs and outputs.
pub struct Builder<'a, P, R, U: sapling::builder::ProverProgress> { pub struct Builder<'a, P, U: sapling::builder::ProverProgress> {
params: P, params: P,
rng: R, build_config: BuildConfig,
target_height: BlockHeight, target_height: BlockHeight,
expiry_height: BlockHeight, expiry_height: BlockHeight,
transparent_builder: TransparentBuilder, transparent_builder: TransparentBuilder,
sapling_builder: SaplingBuilder, sapling_builder: Option<sapling::builder::Builder>,
orchard_builder: Option<orchard::builder::Builder>, orchard_builder: Option<orchard::builder::Builder>,
// TODO: In the future, instead of taking the spending keys as arguments when calling // TODO: In the future, instead of taking the spending keys as arguments when calling
// `add_sapling_spend` or `add_orchard_spend`, we will build an unauthorized, unproven // `add_sapling_spend` or `add_orchard_spend`, we will build an unauthorized, unproven
@ -178,7 +257,7 @@ pub struct Builder<'a, P, R, U: sapling::builder::ProverProgress> {
progress_notifier: U, progress_notifier: U,
} }
impl<'a, P, R, U: sapling::builder::ProverProgress> Builder<'a, P, R, U> { impl<'a, P, U: sapling::builder::ProverProgress> Builder<'a, P, U> {
/// Returns the network parameters that the builder has been configured for. /// Returns the network parameters that the builder has been configured for.
pub fn params(&self) -> &P { pub fn params(&self) -> &P {
&self.params &self.params
@ -204,78 +283,53 @@ impl<'a, P, R, U: sapling::builder::ProverProgress> Builder<'a, P, R, U> {
/// Returns the set of Sapling inputs currently committed to be consumed /// Returns the set of Sapling inputs currently committed to be consumed
/// by the transaction. /// by the transaction.
pub fn sapling_inputs(&self) -> &[sapling::builder::SpendDescriptionInfo] { pub fn sapling_inputs(&self) -> &[sapling::builder::SpendInfo] {
self.sapling_builder.inputs() self.sapling_builder
.as_ref()
.map_or_else(|| &[][..], |b| b.inputs())
} }
/// Returns the set of Sapling outputs currently set to be produced by /// Returns the set of Sapling outputs currently set to be produced by
/// the transaction. /// the transaction.
pub fn sapling_outputs(&self) -> &[sapling::builder::SaplingOutputInfo] { pub fn sapling_outputs(&self) -> &[sapling::builder::OutputInfo] {
self.sapling_builder.outputs() self.sapling_builder
.as_ref()
.map_or_else(|| &[][..], |b| b.outputs())
} }
} }
impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng, ()> { impl<'a, P: consensus::Parameters> Builder<'a, P, ()> {
/// Creates a new `Builder` targeted for inclusion in the block with the given height, /// Creates a new `Builder` targeted for inclusion in the block with the given height,
/// using default values for general transaction fields and the default OS random. /// using default values for general transaction fields.
/// ///
/// # Default values /// # Default values
/// ///
/// The expiry height will be set to the given height plus the default transaction /// The expiry height will be set to the given height plus the default transaction
/// expiry delta (20 blocks). /// expiry delta (20 blocks).
pub fn new( pub fn new(params: P, target_height: BlockHeight, build_config: BuildConfig) -> Self {
params: P,
target_height: BlockHeight,
orchard_anchor: Option<orchard::tree::Anchor>,
) -> Self {
Builder::new_with_rng(params, target_height, orchard_anchor, OsRng)
}
}
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R, ()> {
/// Creates a new `Builder` targeted for inclusion in the block with the given height
/// and randomness source, using default values for general transaction fields.
///
/// # Default values
///
/// The expiry height will be set to the given height plus the default transaction
/// expiry delta (20 blocks).
pub fn new_with_rng(
params: P,
target_height: BlockHeight,
orchard_anchor: Option<orchard::tree::Anchor>,
rng: R,
) -> Self {
let orchard_builder = if params.is_nu_active(NetworkUpgrade::Nu5, target_height) { let orchard_builder = if params.is_nu_active(NetworkUpgrade::Nu5, target_height) {
orchard_anchor.map(|anchor| { let (bundle_type, anchor) = build_config.orchard_builder_config();
orchard::builder::Builder::new( Some(orchard::builder::Builder::new(bundle_type, anchor))
orchard::bundle::Flags::from_parts(true, true),
anchor,
)
})
} else { } else {
None None
}; };
Self::new_internal(params, rng, target_height, orchard_builder) let sapling_builder = Some({
} let (bundle_type, anchor) = build_config.sapling_builder_config();
sapling::builder::Builder::new(
/// Common utility function for builder construction. consensus::sapling_zip212_enforcement(&params, target_height),
fn new_internal( bundle_type,
params: P, anchor,
rng: R, )
target_height: BlockHeight, });
orchard_builder: Option<orchard::builder::Builder>,
) -> Self {
let zip212_enforcement = consensus::sapling_zip212_enforcement(&params, target_height);
Builder { Builder {
params, params,
rng, build_config,
target_height, target_height,
expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA, expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA,
transparent_builder: TransparentBuilder::empty(), transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::new(zip212_enforcement), sapling_builder,
orchard_builder, orchard_builder,
sapling_asks: vec![], sapling_asks: vec![],
orchard_saks: Vec::new(), orchard_saks: Vec::new(),
@ -296,10 +350,10 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R, ()>
pub fn with_progress_notifier( pub fn with_progress_notifier(
self, self,
progress_notifier: Sender<Progress>, progress_notifier: Sender<Progress>,
) -> Builder<'a, P, R, Sender<Progress>> { ) -> Builder<'a, P, Sender<Progress>> {
Builder { Builder {
params: self.params, params: self.params,
rng: self.rng, build_config: self.build_config,
target_height: self.target_height, target_height: self.target_height,
expiry_height: self.expiry_height, expiry_height: self.expiry_height,
transparent_builder: self.transparent_builder, transparent_builder: self.transparent_builder,
@ -313,43 +367,41 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R, ()>
} }
} }
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::ProverProgress> impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder<'a, P, U> {
Builder<'a, P, R, U>
{
/// Adds an Orchard note to be spent in this bundle. /// Adds an Orchard note to be spent in this bundle.
/// ///
/// Returns an error if the given Merkle path does not have the required anchor for /// Returns an error if the given Merkle path does not have the required anchor for
/// the given note. /// the given note.
pub fn add_orchard_spend<FeeError>( pub fn add_orchard_spend<FE>(
&mut self, &mut self,
sk: orchard::keys::SpendingKey, sk: &orchard::keys::SpendingKey,
note: orchard::Note, note: orchard::Note,
merkle_path: orchard::tree::MerklePath, merkle_path: orchard::tree::MerklePath,
) -> Result<(), Error<FeeError>> { ) -> Result<(), Error<FE>> {
self.orchard_builder if let Some(builder) = self.orchard_builder.as_mut() {
.as_mut() builder.add_spend(orchard::keys::FullViewingKey::from(sk), note, merkle_path)?;
.ok_or(Error::OrchardAnchorNotAvailable)?
.add_spend(orchard::keys::FullViewingKey::from(&sk), note, merkle_path)
.map_err(Error::OrchardSpend)?;
self.orchard_saks self.orchard_saks
.push(orchard::keys::SpendAuthorizingKey::from(&sk)); .push(orchard::keys::SpendAuthorizingKey::from(sk));
Ok(()) Ok(())
} else {
Err(Error::OrchardBuilderNotAvailable)
}
} }
/// Adds an Orchard recipient to the transaction. /// Adds an Orchard recipient to the transaction.
pub fn add_orchard_output<FeeError>( pub fn add_orchard_output<FE>(
&mut self, &mut self,
ovk: Option<orchard::keys::OutgoingViewingKey>, ovk: Option<orchard::keys::OutgoingViewingKey>,
recipient: orchard::Address, recipient: orchard::Address,
value: u64, value: u64,
memo: MemoBytes, memo: MemoBytes,
) -> Result<(), Error<FeeError>> { ) -> Result<(), Error<FE>> {
self.orchard_builder self.orchard_builder
.as_mut() .as_mut()
.ok_or(Error::OrchardAnchorNotAvailable)? .ok_or(Error::OrchardBuilderNotAvailable)?
.add_recipient( .add_output(
ovk, ovk,
recipient, recipient,
orchard::value::NoteValue::from_raw(value), orchard::value::NoteValue::from_raw(value),
@ -362,35 +414,40 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
/// ///
/// Returns an error if the given Merkle path does not have the same anchor as the /// Returns an error if the given Merkle path does not have the same anchor as the
/// paths for previous Sapling notes. /// paths for previous Sapling notes.
pub fn add_sapling_spend( pub fn add_sapling_spend<FE>(
&mut self, &mut self,
extsk: sapling::zip32::ExtendedSpendingKey, extsk: &sapling::zip32::ExtendedSpendingKey,
note: Note, note: Note,
merkle_path: sapling::MerklePath, merkle_path: sapling::MerklePath,
) -> Result<(), sapling_builder::Error> { ) -> Result<(), Error<FE>> {
self.sapling_builder if let Some(builder) = self.sapling_builder.as_mut() {
.add_spend(&mut self.rng, &extsk, note, merkle_path)?; builder.add_spend(extsk, note, merkle_path)?;
self.sapling_asks.push(extsk.expsk.ask); self.sapling_asks.push(extsk.expsk.ask.clone());
Ok(())
Ok(()) } else {
Err(Error::SaplingBuilderNotAvailable)
}
} }
/// Adds a Sapling address to send funds to. /// Adds a Sapling address to send funds to.
pub fn add_sapling_output( pub fn add_sapling_output<FE>(
&mut self, &mut self,
ovk: Option<sapling::keys::OutgoingViewingKey>, ovk: Option<sapling::keys::OutgoingViewingKey>,
to: PaymentAddress, to: PaymentAddress,
value: NonNegativeAmount, value: NonNegativeAmount,
memo: MemoBytes, memo: MemoBytes,
) -> Result<(), sapling_builder::Error> { ) -> Result<(), Error<FE>> {
self.sapling_builder.add_output( self.sapling_builder
&mut self.rng, .as_mut()
ovk, .ok_or(Error::SaplingBuilderNotAvailable)?
to, .add_output(
sapling::value::NoteValue::from_raw(value.into()), ovk,
Some(*memo.as_array()), to,
) sapling::value::NoteValue::from_raw(value.into()),
Some(*memo.as_array()),
)
.map_err(Error::SaplingBuild)
} }
/// Adds a transparent coin to be spent in this transaction. /// Adds a transparent coin to be spent in this transaction.
@ -418,14 +475,17 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
fn value_balance(&self) -> Result<Amount, BalanceError> { fn value_balance(&self) -> Result<Amount, BalanceError> {
let value_balances = [ let value_balances = [
self.transparent_builder.value_balance()?, self.transparent_builder.value_balance()?,
self.sapling_builder.value_balance(), self.sapling_builder
if let Some(builder) = &self.orchard_builder { .as_ref()
builder .map_or_else(Amount::zero, |builder| builder.value_balance::<Amount>()),
.value_balance() self.orchard_builder.as_ref().map_or_else(
.map_err(|_| BalanceError::Overflow)? || Ok(Amount::zero()),
} else { |builder| {
Amount::zero() builder
}, .value_balance::<Amount>()
.map_err(|_| BalanceError::Overflow)
},
)?,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
self.tze_builder.value_balance()?, self.tze_builder.value_balance()?,
]; ];
@ -440,46 +500,102 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
/// ///
/// This fee is a function of the spends and outputs that have been added to the builder, /// This fee is a function of the spends and outputs that have been added to the builder,
/// pursuant to the specified [`FeeRule`]. /// pursuant to the specified [`FeeRule`].
pub fn get_fee<FR: FeeRule>(&self, fee_rule: &FR) -> Result<NonNegativeAmount, FR::Error> { pub fn get_fee<FR: FeeRule>(
&self,
fee_rule: &FR,
) -> Result<NonNegativeAmount, FeeError<FR::Error>> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let transparent_inputs = self.transparent_builder.inputs(); let transparent_inputs = self.transparent_builder.inputs();
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs: &[Infallible] = &[]; let transparent_inputs: &[Infallible] = &[];
fee_rule.fee_required( let sapling_spends = self
&self.params, .sapling_builder
self.target_height, .as_ref()
transparent_inputs, .map_or(0, |builder| builder.inputs().len());
self.transparent_builder.outputs(),
self.sapling_builder.inputs().len(), fee_rule
self.sapling_builder.bundle_output_count(), .fee_required(
match std::cmp::max( &self.params,
self.orchard_builder self.target_height,
.as_ref() transparent_inputs,
.map_or(0, |builder| builder.outputs().len()), self.transparent_builder.outputs(),
self.orchard_builder sapling_spends,
.as_ref() self.sapling_builder.as_ref().map_or(Ok(0), |builder| {
.map_or(0, |builder| builder.spends().len()), self.build_config
) { .sapling_builder_config()
1 => 2, .0
n => n, .num_outputs(sapling_spends, builder.outputs().len())
}, .map_err(FeeError::Bundle)
) })?,
self.orchard_builder.as_ref().map_or(Ok(0), |builder| {
self.build_config
.orchard_builder_config()
.0
.num_actions(builder.spends().len(), builder.outputs().len())
.map_err(FeeError::Bundle)
})?,
)
.map_err(FeeError::FeeRule)
}
#[cfg(feature = "zfuture")]
pub fn get_fee_zfuture<FR: FeeRule + FutureFeeRule>(
&self,
fee_rule: &FR,
) -> Result<NonNegativeAmount, 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());
fee_rule
.fee_required_zfuture(
&self.params,
self.target_height,
transparent_inputs,
self.transparent_builder.outputs(),
sapling_spends,
self.sapling_builder.as_ref().map_or(Ok(0), |builder| {
self.build_config
.sapling_builder_config()
.0
.num_outputs(sapling_spends, builder.outputs().len())
.map_err(FeeError::Bundle)
})?,
self.orchard_builder.as_ref().map_or(Ok(0), |builder| {
self.build_config
.orchard_builder_config()
.0
.num_actions(builder.spends().len(), builder.outputs().len())
.map_err(FeeError::Bundle)
})?,
self.tze_builder.inputs(),
self.tze_builder.outputs(),
)
.map_err(FeeError::FeeRule)
} }
/// Builds a transaction from the configured spends and outputs. /// Builds a transaction from the configured spends and outputs.
/// ///
/// Upon success, returns a tuple containing the final transaction, and the /// Upon success, returns a tuple containing the final transaction, and the
/// [`SaplingMetadata`] generated during the build process. /// [`SaplingMetadata`] generated during the build process.
pub fn build<SP: SpendProver, OP: OutputProver, FR: FeeRule>( pub fn build<R: RngCore + CryptoRng, SP: SpendProver, OP: OutputProver, FR: FeeRule>(
self, self,
rng: R,
spend_prover: &SP, spend_prover: &SP,
output_prover: &OP, output_prover: &OP,
fee_rule: &FR, fee_rule: &FR,
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> { ) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
let fee = self.get_fee(fee_rule).map_err(Error::Fee)?; let fee = self.get_fee(fee_rule).map_err(Error::Fee)?;
self.build_internal(spend_prover, output_prover, fee.into()) self.build_internal(rng, spend_prover, output_prover, fee)
} }
/// Builds a transaction from the configured spends and outputs. /// Builds a transaction from the configured spends and outputs.
@ -487,47 +603,28 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
/// Upon success, returns a tuple containing the final transaction, and the /// Upon success, returns a tuple containing the final transaction, and the
/// [`SaplingMetadata`] generated during the build process. /// [`SaplingMetadata`] generated during the build process.
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
pub fn build_zfuture<SP: SpendProver, OP: OutputProver, FR: FutureFeeRule>( pub fn build_zfuture<
R: RngCore + CryptoRng,
SP: SpendProver,
OP: OutputProver,
FR: FutureFeeRule,
>(
self, self,
rng: R,
spend_prover: &SP, spend_prover: &SP,
output_prover: &OP, output_prover: &OP,
fee_rule: &FR, fee_rule: &FR,
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> { ) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
#[cfg(feature = "transparent-inputs")] let fee = self.get_fee_zfuture(fee_rule).map_err(Error::Fee)?;
let transparent_inputs = self.transparent_builder.inputs(); self.build_internal(rng, spend_prover, output_prover, fee)
#[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs: &[Infallible] = &[];
let fee = fee_rule
.fee_required_zfuture(
&self.params,
self.target_height,
transparent_inputs,
self.transparent_builder.outputs(),
self.sapling_builder.inputs().len(),
self.sapling_builder.bundle_output_count(),
std::cmp::max(
self.orchard_builder
.as_ref()
.map_or(0, |builder| builder.outputs().len()),
self.orchard_builder
.as_ref()
.map_or(0, |builder| builder.spends().len()),
),
self.tze_builder.inputs(),
self.tze_builder.outputs(),
)
.map_err(Error::Fee)?;
self.build_internal(spend_prover, output_prover, fee.into())
} }
fn build_internal<SP: SpendProver, OP: OutputProver, FE>( fn build_internal<R: RngCore + CryptoRng, SP: SpendProver, OP: OutputProver, FE>(
self, self,
mut rng: R,
spend_prover: &SP, spend_prover: &SP,
output_prover: &OP, output_prover: &OP,
fee: Amount, fee: NonNegativeAmount,
) -> Result<(Transaction, SaplingMetadata), Error<FE>> { ) -> Result<(Transaction, SaplingMetadata), Error<FE>> {
let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); let consensus_branch_id = BranchId::for_height(&self.params, self.target_height);
@ -539,7 +636,8 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
// //
// After fees are accounted for, the value balance of the transaction must be zero. // After fees are accounted for, the value balance of the transaction must be zero.
let balance_after_fees = (self.value_balance()? - fee).ok_or(BalanceError::Underflow)?; let balance_after_fees =
(self.value_balance()? - fee.into()).ok_or(BalanceError::Underflow)?;
match balance_after_fees.cmp(&Amount::zero()) { match balance_after_fees.cmp(&Amount::zero()) {
Ordering::Less => { Ordering::Less => {
@ -553,35 +651,45 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
let transparent_bundle = self.transparent_builder.build(); let transparent_bundle = self.transparent_builder.build();
let mut rng = self.rng;
let (sapling_bundle, tx_metadata) = match self let (sapling_bundle, tx_metadata) = match self
.sapling_builder .sapling_builder
.build::<SP, OP, _, _>(&mut rng) .and_then(|builder| {
.map_err(Error::SaplingBuild)? builder
.map(|(bundle, tx_metadata)| { .build::<SP, OP, _, _>(&mut rng)
// We need to create proofs before signatures, because we still support .map_err(Error::SaplingBuild)
// creating V4 transactions, which commit to the Sapling proofs in the .transpose()
// transaction digest. .map(|res| {
( res.map(|(bundle, tx_metadata)| {
bundle.create_proofs( // We need to create proofs before signatures, because we still support
spend_prover, // creating V4 transactions, which commit to the Sapling proofs in the
output_prover, // transaction digest.
&mut rng, (
self.progress_notifier, bundle.create_proofs(
), spend_prover,
tx_metadata, output_prover,
) &mut rng,
}) { self.progress_notifier,
),
tx_metadata,
)
})
})
})
.transpose()?
{
Some((bundle, meta)) => (Some(bundle), meta), Some((bundle, meta)) => (Some(bundle), meta),
None => (None, SaplingMetadata::empty()), None => (None, SaplingMetadata::empty()),
}; };
let orchard_bundle: Option<orchard::Bundle<_, Amount>> = let orchard_bundle: Option<orchard::Bundle<_, Amount>> = self
if let Some(builder) = self.orchard_builder { .orchard_builder
Some(builder.build(&mut rng).map_err(Error::OrchardBuild)?) .and_then(|builder| {
} else { builder
None .build(&mut rng)
}; .map_err(Error::OrchardBuild)
.transpose()
})
.transpose()?;
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
let (tze_bundle, tze_signers) = self.tze_builder.build(); let (tze_bundle, tze_signers) = self.tze_builder.build();
@ -674,8 +782,8 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::
} }
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng, U: sapling::builder::ProverProgress> impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> ExtensionTxBuilder<'a>
ExtensionTxBuilder<'a> for Builder<'a, P, R, U> for Builder<'a, P, U>
{ {
type BuildCtx = TransactionData<Unauthorized>; type BuildCtx = TransactionData<Unauthorized>;
type BuildError = tze::builder::Error; type BuildError = tze::builder::Error;
@ -714,7 +822,7 @@ mod testing {
use super::{Builder, Error, SaplingMetadata}; use super::{Builder, Error, SaplingMetadata};
use crate::{ use crate::{
consensus::{self, BlockHeight}, consensus,
sapling::{ sapling::{
self, self,
prover::mock::{MockOutputProver, MockSpendProver}, prover::mock::{MockOutputProver, MockSpendProver},
@ -723,21 +831,13 @@ mod testing {
transaction::Transaction, transaction::Transaction,
}; };
impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R, ()> { impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder<'a, P, U> {
/// Creates a new `Builder` targeted for inclusion in the block with the given height /// Build the transaction using mocked randomness and proving capabilities.
/// and randomness source, using default values for general transaction fields. /// DO NOT USE EXCEPT FOR UNIT TESTING.
/// pub fn mock_build<R: RngCore>(
/// # Default values self,
///
/// The expiry height will be set to the given height plus the default transaction
/// expiry delta.
///
/// WARNING: DO NOT USE IN PRODUCTION
pub fn test_only_new_with_rng(
params: P,
height: BlockHeight,
rng: R, rng: R,
) -> Builder<'a, P, impl RngCore + CryptoRng, ()> { ) -> Result<(Transaction, SaplingMetadata), Error<Infallible>> {
struct FakeCryptoRng<R: RngCore>(R); struct FakeCryptoRng<R: RngCore>(R);
impl<R: RngCore> CryptoRng for FakeCryptoRng<R> {} impl<R: RngCore> CryptoRng for FakeCryptoRng<R> {}
impl<R: RngCore> RngCore for FakeCryptoRng<R> { impl<R: RngCore> RngCore for FakeCryptoRng<R> {
@ -757,19 +857,10 @@ mod testing {
self.0.try_fill_bytes(dest) self.0.try_fill_bytes(dest)
} }
} }
Builder::new_internal(params, FakeCryptoRng(rng), height, None)
}
}
impl<
'a,
P: consensus::Parameters,
R: RngCore + CryptoRng,
U: sapling::builder::ProverProgress,
> Builder<'a, P, R, U>
{
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error<Infallible>> {
#[allow(deprecated)] #[allow(deprecated)]
self.build( self.build(
FakeCryptoRng(rng),
&MockSpendProver, &MockSpendProver,
&MockOutputProver, &MockOutputProver,
&fixed::FeeRule::standard(), &fixed::FeeRule::standard(),
@ -780,6 +871,8 @@ mod testing {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::convert::Infallible;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use ff::Field; use ff::Field;
use incrementalmerkletree::{frontier::CommitmentTree, witness::IncrementalWitness}; use incrementalmerkletree::{frontier::CommitmentTree, witness::IncrementalWitness};
@ -790,7 +883,10 @@ mod tests {
legacy::TransparentAddress, legacy::TransparentAddress,
memo::MemoBytes, memo::MemoBytes,
sapling::{self, zip32::ExtendedSpendingKey, Node, Rseed}, sapling::{self, zip32::ExtendedSpendingKey, Node, Rseed},
transaction::components::amount::{Amount, BalanceError, NonNegativeAmount}, transaction::{
builder::BuildConfig,
components::amount::{Amount, BalanceError, NonNegativeAmount},
},
}; };
use super::{Builder, Error}; use super::{Builder, Error};
@ -802,11 +898,7 @@ mod tests {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use crate::{ use crate::{
legacy::keys::{AccountPrivKey, IncomingViewingKey}, legacy::keys::{AccountPrivKey, IncomingViewingKey},
sapling::note_encryption::Zip212Enforcement, transaction::{builder::DEFAULT_TX_EXPIRY_DELTA, OutPoint, TxOut},
transaction::{
builder::{SaplingBuilder, DEFAULT_TX_EXPIRY_DELTA},
OutPoint, TxOut,
},
zip32::AccountId, zip32::AccountId,
}; };
@ -825,11 +917,14 @@ mod tests {
// Create a builder with 0 fee, so we can construct t outputs // Create a builder with 0 fee, so we can construct t outputs
let mut builder = builder::Builder { let mut builder = builder::Builder {
params: TEST_NETWORK, params: TEST_NETWORK,
rng: OsRng, build_config: BuildConfig::Standard {
sapling_anchor: sapling::Anchor::empty_tree(),
orchard_anchor: orchard::Anchor::empty_tree(),
},
target_height: sapling_activation_height, target_height: sapling_activation_height,
expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA, expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA,
transparent_builder: TransparentBuilder::empty(), transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::new(Zip212Enforcement::Off), sapling_builder: None,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(), tze_builder: TzeBuilder::empty(),
#[cfg(not(feature = "zfuture"))] #[cfg(not(feature = "zfuture"))]
@ -867,7 +962,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let (tx, _) = builder.mock_build().unwrap(); let (tx, _) = builder.mock_build(OsRng).unwrap();
// No binding signature, because only t input and outputs // No binding signature, because only t input and outputs
assert!(tx.sapling_bundle.is_none()); assert!(tx.sapling_bundle.is_none());
} }
@ -892,11 +987,16 @@ mod tests {
let tx_height = TEST_NETWORK let tx_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling) .activation_height(NetworkUpgrade::Sapling)
.unwrap(); .unwrap();
let mut builder = Builder::new(TEST_NETWORK, tx_height, None);
let build_config = BuildConfig::Standard {
sapling_anchor: witness1.root().into(),
orchard_anchor: orchard::Anchor::empty_tree(),
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
// Create a tx with a sapling spend. binding_sig should be present // Create a tx with a sapling spend. binding_sig should be present
builder builder
.add_sapling_spend(extsk, note1, witness1.path().unwrap()) .add_sapling_spend::<Infallible>(&extsk, note1, witness1.path().unwrap())
.unwrap(); .unwrap();
builder builder
@ -907,7 +1007,7 @@ mod tests {
.unwrap(); .unwrap();
// A binding signature (and bundle) is present because there is a Sapling spend. // A binding signature (and bundle) is present because there is a Sapling spend.
let (tx, _) = builder.mock_build().unwrap(); let (tx, _) = builder.mock_build(OsRng).unwrap();
assert!(tx.sapling_bundle().is_some()); assert!(tx.sapling_bundle().is_some());
} }
@ -926,9 +1026,13 @@ mod tests {
// Fails with no inputs or outputs // Fails with no inputs or outputs
// 0.0001 t-ZEC fee // 0.0001 t-ZEC fee
{ {
let builder = Builder::new(TEST_NETWORK, tx_height, None); let build_config = BuildConfig::Standard {
sapling_anchor: sapling::Anchor::empty_tree(),
orchard_anchor: orchard::Anchor::empty_tree(),
};
let builder = Builder::new(TEST_NETWORK, tx_height, build_config);
assert_matches!( assert_matches!(
builder.mock_build(), builder.mock_build(OsRng),
Err(Error::InsufficientFunds(expected)) if expected == MINIMUM_FEE.into() Err(Error::InsufficientFunds(expected)) if expected == MINIMUM_FEE.into()
); );
} }
@ -940,9 +1044,13 @@ mod tests {
// Fail if there is only a Sapling output // Fail if there is only a Sapling output
// 0.0005 z-ZEC out, 0.0001 t-ZEC fee // 0.0005 z-ZEC out, 0.0001 t-ZEC fee
{ {
let mut builder = Builder::new(TEST_NETWORK, tx_height, None); let build_config = BuildConfig::Standard {
sapling_anchor: sapling::Anchor::empty_tree(),
orchard_anchor: orchard::Anchor::empty_tree(),
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder builder
.add_sapling_output( .add_sapling_output::<Infallible>(
ovk, ovk,
to, to,
NonNegativeAmount::const_from_u64(50000), NonNegativeAmount::const_from_u64(50000),
@ -950,7 +1058,7 @@ mod tests {
) )
.unwrap(); .unwrap();
assert_matches!( assert_matches!(
builder.mock_build(), builder.mock_build(OsRng),
Err(Error::InsufficientFunds(expected)) if Err(Error::InsufficientFunds(expected)) if
expected == (NonNegativeAmount::const_from_u64(50000) + MINIMUM_FEE).unwrap().into() expected == (NonNegativeAmount::const_from_u64(50000) + MINIMUM_FEE).unwrap().into()
); );
@ -959,7 +1067,11 @@ mod tests {
// Fail if there is only a transparent output // Fail if there is only a transparent output
// 0.0005 t-ZEC out, 0.0001 t-ZEC fee // 0.0005 t-ZEC out, 0.0001 t-ZEC fee
{ {
let mut builder = Builder::new(TEST_NETWORK, tx_height, None); let build_config = BuildConfig::Standard {
sapling_anchor: sapling::Anchor::empty_tree(),
orchard_anchor: orchard::Anchor::empty_tree(),
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder builder
.add_transparent_output( .add_transparent_output(
&TransparentAddress::PublicKey([0; 20]), &TransparentAddress::PublicKey([0; 20]),
@ -967,7 +1079,7 @@ mod tests {
) )
.unwrap(); .unwrap();
assert_matches!( assert_matches!(
builder.mock_build(), builder.mock_build(OsRng),
Err(Error::InsufficientFunds(expected)) if expected == Err(Error::InsufficientFunds(expected)) if expected ==
(NonNegativeAmount::const_from_u64(50000) + MINIMUM_FEE).unwrap().into() (NonNegativeAmount::const_from_u64(50000) + MINIMUM_FEE).unwrap().into()
); );
@ -985,12 +1097,16 @@ mod tests {
// Fail if there is insufficient input // Fail if there is insufficient input
// 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in
{ {
let mut builder = Builder::new(TEST_NETWORK, tx_height, None); let build_config = BuildConfig::Standard {
sapling_anchor: witness1.root().into(),
orchard_anchor: orchard::Anchor::empty_tree(),
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder builder
.add_sapling_spend(extsk.clone(), note1.clone(), witness1.path().unwrap()) .add_sapling_spend::<Infallible>(&extsk, note1.clone(), witness1.path().unwrap())
.unwrap(); .unwrap();
builder builder
.add_sapling_output( .add_sapling_output::<Infallible>(
ovk, ovk,
to, to,
NonNegativeAmount::const_from_u64(30000), NonNegativeAmount::const_from_u64(30000),
@ -1004,7 +1120,7 @@ mod tests {
) )
.unwrap(); .unwrap();
assert_matches!( assert_matches!(
builder.mock_build(), builder.mock_build(OsRng),
Err(Error::InsufficientFunds(expected)) if expected == Amount::const_from_i64(1) Err(Error::InsufficientFunds(expected)) if expected == Amount::const_from_i64(1)
); );
} }
@ -1021,15 +1137,19 @@ mod tests {
// Succeeds if there is sufficient input // Succeeds if there is sufficient input
// 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.0006 z-ZEC in // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.0006 z-ZEC in
{ {
let mut builder = Builder::new(TEST_NETWORK, tx_height, None); let build_config = BuildConfig::Standard {
sapling_anchor: witness1.root().into(),
orchard_anchor: orchard::Anchor::empty_tree(),
};
let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config);
builder builder
.add_sapling_spend(extsk.clone(), note1, witness1.path().unwrap()) .add_sapling_spend::<Infallible>(&extsk, note1, witness1.path().unwrap())
.unwrap(); .unwrap();
builder builder
.add_sapling_spend(extsk, note2, witness2.path().unwrap()) .add_sapling_spend::<Infallible>(&extsk, note2, witness2.path().unwrap())
.unwrap(); .unwrap();
builder builder
.add_sapling_output( .add_sapling_output::<Infallible>(
ovk, ovk,
to, to,
NonNegativeAmount::const_from_u64(30000), NonNegativeAmount::const_from_u64(30000),
@ -1043,7 +1163,7 @@ mod tests {
) )
.unwrap(); .unwrap();
assert_matches!( assert_matches!(
builder.mock_build(), builder.mock_build(OsRng),
Ok((tx, _)) if tx.fee_paid(|_| Err(BalanceError::Overflow)).unwrap() == Amount::const_from_i64(10_000) Ok((tx, _)) if tx.fee_paid(|_| Err(BalanceError::Overflow)).unwrap() == Amount::const_from_i64(10_000)
); );
} }

View File

@ -9,8 +9,8 @@ use zcash_note_encryption::{EphemeralKeyBytes, ENC_CIPHERTEXT_SIZE, OUT_CIPHERTE
use crate::{ use crate::{
sapling::{ sapling::{
bundle::{ bundle::{
Authorized, Bundle, GrothProofBytes, OutputDescription, OutputDescriptionV5, Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription,
SpendDescription, SpendDescriptionV5, OutputDescriptionV5, SpendDescription, SpendDescriptionV5,
}, },
note::ExtractedNoteCommitment, note::ExtractedNoteCommitment,
value::ValueCommitment, value::ValueCommitment,
@ -21,6 +21,51 @@ use crate::{
use super::{Amount, GROTH_PROOF_SIZE}; use super::{Amount, GROTH_PROOF_SIZE};
/// A map from one bundle authorization to another.
///
/// For use with [`TransactionData::map_authorization`].
///
/// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization
pub trait MapAuth<A: Authorization, B: Authorization> {
fn map_spend_proof(&mut self, p: A::SpendProof) -> B::SpendProof;
fn map_output_proof(&mut self, p: A::OutputProof) -> B::OutputProof;
fn map_auth_sig(&mut self, s: A::AuthSig) -> B::AuthSig;
fn map_authorization(&mut self, a: A) -> B;
}
/// The identity map.
///
/// This can be used with [`TransactionData::map_authorization`] when you want to map the
/// authorization of a subset of a transaction's bundles.
///
/// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization
impl MapAuth<Authorized, Authorized> for () {
fn map_spend_proof(
&mut self,
p: <Authorized as Authorization>::SpendProof,
) -> <Authorized as Authorization>::SpendProof {
p
}
fn map_output_proof(
&mut self,
p: <Authorized as Authorization>::OutputProof,
) -> <Authorized as Authorization>::OutputProof {
p
}
fn map_auth_sig(
&mut self,
s: <Authorized as Authorization>::AuthSig,
) -> <Authorized as Authorization>::AuthSig {
s
}
fn map_authorization(&mut self, a: Authorized) -> Authorized {
a
}
}
/// Consensus rules (§4.4) & (§4.5): /// Consensus rules (§4.4) & (§4.5):
/// - Canonical encoding is enforced here. /// - Canonical encoding is enforced here.
/// - "Not small order" is enforced here. /// - "Not small order" is enforced here.

View File

@ -488,7 +488,7 @@ impl<A: Authorization> TransactionData<A> {
pub fn map_authorization<B: Authorization>( pub fn map_authorization<B: Authorization>(
self, self,
f_transparent: impl transparent::MapAuth<A::TransparentAuth, B::TransparentAuth>, f_transparent: impl transparent::MapAuth<A::TransparentAuth, B::TransparentAuth>,
f_sapling: impl sapling::bundle::MapAuth<A::SaplingAuth, B::SaplingAuth>, mut f_sapling: impl sapling_serialization::MapAuth<A::SaplingAuth, B::SaplingAuth>,
mut f_orchard: impl orchard_serialization::MapAuth<A::OrchardAuth, B::OrchardAuth>, mut f_orchard: impl orchard_serialization::MapAuth<A::OrchardAuth, B::OrchardAuth>,
#[cfg(feature = "zfuture")] f_tze: impl tze::MapAuth<A::TzeAuth, B::TzeAuth>, #[cfg(feature = "zfuture")] f_tze: impl tze::MapAuth<A::TzeAuth, B::TzeAuth>,
) -> TransactionData<B> { ) -> TransactionData<B> {
@ -501,7 +501,15 @@ impl<A: Authorization> TransactionData<A> {
.transparent_bundle .transparent_bundle
.map(|b| b.map_authorization(f_transparent)), .map(|b| b.map_authorization(f_transparent)),
sprout_bundle: self.sprout_bundle, sprout_bundle: self.sprout_bundle,
sapling_bundle: self.sapling_bundle.map(|b| b.map_authorization(f_sapling)), sapling_bundle: self.sapling_bundle.map(|b| {
b.map_authorization(
&mut f_sapling,
|f, p| f.map_spend_proof(p),
|f, p| f.map_output_proof(p),
|f, s| f.map_auth_sig(s),
|f, a| f.map_authorization(a),
)
}),
orchard_bundle: self.orchard_bundle.map(|b| { orchard_bundle: self.orchard_bundle.map(|b| {
b.map_authorization( b.map_authorization(
&mut f_orchard, &mut f_orchard,