diff --git a/CHANGELOG.md b/CHANGELOG.md index c0bc6b6f2..b7568b825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `orchard::builder::BundleType` + +### Changed +- `orchard::builder::Builder::add_recipient` has been renamed to `add_output` + in order to clarify than more than one output of a given transaction may be + sent to the same recipient. +- `orchard::builder::Builder::build` now takes an additional `BundleType` argument + that specifies how actions should be padded, instead of using hardcoded padding. ## [0.6.0] - 2023-09-08 ### Changed @@ -22,8 +31,8 @@ and this project adheres to Rust's notion of - `orchard::builder`: - `{SpendInfo::new, InputView, OutputView}` - `Builder::{spends, outputs}` - - `SpendError` - - `OutputError` + - `SpendError` + - `OutputError` ### Changed - MSRV is now 1.60.0. diff --git a/benches/circuit.rs b/benches/circuit.rs index 3140a90a4..3bc6d3a37 100644 --- a/benches/circuit.rs +++ b/benches/circuit.rs @@ -7,7 +7,7 @@ use criterion::{BenchmarkId, Criterion}; use pprof::criterion::{Output, PProfProfiler}; use orchard::{ - builder::Builder, + builder::{Builder, BundleType}, bundle::Flags, circuit::{ProvingKey, VerifyingKey}, keys::{FullViewingKey, Scope, SpendingKey}, @@ -32,10 +32,10 @@ fn criterion_benchmark(c: &mut Criterion) { ); for _ in 0..num_recipients { builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_output(None, recipient, NoteValue::from_raw(10), None) .unwrap(); } - let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); + let bundle: Bundle<_, i64> = builder.build(rng, &BundleType::Transactional).unwrap(); let instances: Vec<_> = bundle .actions() diff --git a/benches/note_decryption.rs b/benches/note_decryption.rs index 3ebbc6ed5..6c560f6dd 100644 --- a/benches/note_decryption.rs +++ b/benches/note_decryption.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use orchard::{ - builder::Builder, + builder::{Builder, BundleType}, bundle::Flags, circuit::ProvingKey, keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendingKey}, @@ -52,12 +52,12 @@ fn bench_note_decryption(c: &mut Criterion) { // The builder pads to two actions, and shuffles their order. Add two recipients // so the first action is always decryptable. builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_output(None, recipient, NoteValue::from_raw(10), None) .unwrap(); builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_output(None, recipient, NoteValue::from_raw(10), None) .unwrap(); - let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); + let bundle: Bundle<_, i64> = builder.build(rng, &BundleType::Transactional).unwrap(); bundle .create_proof(&pk, rng) .unwrap() diff --git a/src/builder.rs b/src/builder.rs index b2d747b7c..c9361d0e1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -27,6 +27,42 @@ use crate::{ const MIN_ACTIONS: usize = 2; +/// An enumeration of rules Orchard bundle construction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BundleType { + /// A transactional bundle will be padded if necessary to contain at least 2 actions, + /// irrespective of whether any genuine actions are required. + Transactional, + /// A coinbase bundle is required to have no non-dummy spends. No padding is performed. + Coinbase, +} + +impl BundleType { + /// Returns the number of logical actions that builder will produce in constructing a bundle + /// of this type, given the specified numbers of spends and outputs. + /// + /// Returns an error if the specified number of spends and outputs is incompatible with + /// this bundle type. + pub fn num_actions( + &self, + num_spends: usize, + num_outputs: usize, + ) -> Result { + let num_real_actions = core::cmp::max(num_spends, num_outputs); + + match self { + BundleType::Transactional => Ok(core::cmp::max(num_real_actions, MIN_ACTIONS)), + BundleType::Coinbase => { + if num_spends == 0 { + Ok(num_real_actions) + } else { + Err("Spends not allowed in coinbase bundles") + } + } + } + } +} + /// An error type for the kinds of errors that can occur during bundle construction. #[derive(Debug)] pub enum BuildError { @@ -42,6 +78,8 @@ pub enum BuildError { /// A signature is valid for more than one input. This should never happen if `alpha` /// is sampled correctly, and indicates a critical failure in randomness generation. DuplicateSignature, + /// The bundle being constructed violated the construction rules for the requested bundle type. + BundleTypeNotSatisfiable, } impl Display for BuildError { @@ -53,6 +91,9 @@ impl Display for BuildError { ValueSum(_) => f.write_str("Overflow occurred during value construction"), InvalidExternalSignature => f.write_str("External signature was invalid"), DuplicateSignature => f.write_str("Signature valid for more than one input"), + BundleTypeNotSatisfiable => { + f.write_str("Bundle structure did not conform to requested bundle type.") + } } } } @@ -157,16 +198,16 @@ impl SpendInfo { } } -/// Information about a specific recipient to receive funds in an [`Action`]. +/// Information about a specific output to receive funds in an [`Action`]. #[derive(Debug)] -struct RecipientInfo { +struct OutputInfo { ovk: Option, recipient: Address, value: NoteValue, memo: Option<[u8; 512]>, } -impl RecipientInfo { +impl OutputInfo { /// Defined in [Zcash Protocol Spec ยง 4.8.3: Dummy Notes (Orchard)][orcharddummynotes]. /// /// [orcharddummynotes]: https://zips.z.cash/protocol/nu5.pdf#orcharddummynotes @@ -174,7 +215,7 @@ impl RecipientInfo { let fvk: FullViewingKey = (&SpendingKey::random(rng)).into(); let recipient = fvk.address_at(0u32, Scope::External); - RecipientInfo { + OutputInfo { ovk: None, recipient, value: NoteValue::zero(), @@ -187,12 +228,12 @@ impl RecipientInfo { #[derive(Debug)] struct ActionInfo { spend: SpendInfo, - output: RecipientInfo, + output: OutputInfo, rcv: ValueCommitTrapdoor, } impl ActionInfo { - fn new(spend: SpendInfo, output: RecipientInfo, rng: impl RngCore) -> Self { + fn new(spend: SpendInfo, output: OutputInfo, rng: impl RngCore) -> Self { ActionInfo { spend, output, @@ -256,12 +297,12 @@ impl ActionInfo { } } -/// A builder that constructs a [`Bundle`] from a set of notes to be spent, and recipients +/// A builder that constructs a [`Bundle`] from a set of notes to be spent, and outputs /// to receive funds. #[derive(Debug)] pub struct Builder { spends: Vec, - recipients: Vec, + outputs: Vec, flags: Flags, anchor: Anchor, } @@ -271,7 +312,7 @@ impl Builder { pub fn new(flags: Flags, anchor: Anchor) -> Self { Builder { spends: vec![], - recipients: vec![], + outputs: vec![], flags, anchor, } @@ -323,7 +364,7 @@ impl Builder { } /// Adds an address which will receive funds in this transaction. - pub fn add_recipient( + pub fn add_output( &mut self, ovk: Option, recipient: Address, @@ -334,7 +375,7 @@ impl Builder { return Err(OutputError); } - self.recipients.push(RecipientInfo { + self.outputs.push(OutputInfo { ovk, recipient, value, @@ -353,7 +394,7 @@ impl Builder { /// Returns the action output components that will be produced by the /// transaction being constructed pub fn outputs(&self) -> &Vec { - &self.recipients + &self.outputs } /// The net value of the bundle to be built. The value of all spends, @@ -372,51 +413,51 @@ impl Builder { .iter() .map(|spend| spend.note.value() - NoteValue::zero()) .chain( - self.recipients + self.outputs .iter() - .map(|recipient| NoteValue::zero() - recipient.value), + .map(|output| NoteValue::zero() - output.value), ) .fold(Some(ValueSum::zero()), |acc, note_value| acc? + note_value) .ok_or(OverflowError)?; i64::try_from(value_balance).and_then(|i| V::try_from(i).map_err(|_| value::OverflowError)) } - /// Builds a bundle containing the given spent notes and recipients. + /// Builds a bundle containing the given spent notes and outputs. /// /// The returned bundle will have no proof or signatures; these can be applied with /// [`Bundle::create_proof`] and [`Bundle::apply_signatures`] respectively. pub fn build>( mut self, mut rng: impl RngCore, + bundle_type: &BundleType, ) -> Result, V>, BuildError> { - // Pair up the spends and recipients, extending with dummy values as necessary. - let pre_actions: Vec<_> = { - let num_spends = self.spends.len(); - let num_recipients = self.recipients.len(); - let num_actions = [num_spends, num_recipients, MIN_ACTIONS] - .iter() - .max() - .cloned() - .unwrap(); + let num_real_spends = self.spends.len(); + let num_real_outputs = self.outputs.len(); + let num_actions = bundle_type + .num_actions(num_real_spends, num_real_outputs) + .map_err(|_| BuildError::BundleTypeNotSatisfiable)?; + // Pair up the spends and outputs, extending with dummy values as necessary. + let pre_actions: Vec<_> = { self.spends.extend( - iter::repeat_with(|| SpendInfo::dummy(&mut rng)).take(num_actions - num_spends), + iter::repeat_with(|| SpendInfo::dummy(&mut rng)) + .take(num_actions - num_real_spends), ); - self.recipients.extend( - iter::repeat_with(|| RecipientInfo::dummy(&mut rng)) - .take(num_actions - num_recipients), + self.outputs.extend( + iter::repeat_with(|| OutputInfo::dummy(&mut rng)) + .take(num_actions - num_real_outputs), ); - // Shuffle the spends and recipients, so that learning the position of a + // Shuffle the spends and outputs, so that learning the position of a // specific spent note or output note doesn't reveal anything on its own about // the meaning of that note in the transaction context. self.spends.shuffle(&mut rng); - self.recipients.shuffle(&mut rng); + self.outputs.shuffle(&mut rng); self.spends .into_iter() - .zip(self.recipients.into_iter()) - .map(|(spend, recipient)| ActionInfo::new(spend, recipient, &mut rng)) + .zip(self.outputs.into_iter()) + .map(|(spend, output)| ActionInfo::new(spend, output, &mut rng)) .collect() }; @@ -749,7 +790,7 @@ pub trait OutputView { fn value>(&self) -> V; } -impl OutputView for RecipientInfo { +impl OutputView for OutputInfo { fn value>(&self) -> V { V::from(self.value.inner()) } @@ -777,7 +818,7 @@ pub mod testing { Address, Note, }; - use super::Builder; + use super::{Builder, BundleType}; /// An intermediate type used for construction of arbitrary /// bundle values. This type is required because of a limitation @@ -793,7 +834,7 @@ pub mod testing { sk: SpendingKey, anchor: Anchor, notes: Vec<(Note, MerklePath)>, - recipient_amounts: Vec<(Address, NoteValue)>, + output_amounts: Vec<(Address, NoteValue)>, } impl ArbitraryBundleInputs { @@ -807,18 +848,18 @@ pub mod testing { builder.add_spend(fvk.clone(), note, path).unwrap(); } - for (addr, value) in self.recipient_amounts.into_iter() { + for (addr, value) in self.output_amounts.into_iter() { let scope = fvk.scope_for_address(&addr).unwrap(); let ovk = fvk.to_ovk(scope); builder - .add_recipient(Some(ovk.clone()), addr, value, None) + .add_output(Some(ovk.clone()), addr, value, None) .unwrap(); } let pk = ProvingKey::build(); builder - .build(&mut self.rng) + .build(&mut self.rng, &BundleType::Transactional) .unwrap() .create_proof(&pk, &mut self.rng) .unwrap() @@ -834,7 +875,7 @@ pub mod testing { fn arb_bundle_inputs(sk: SpendingKey) ( n_notes in 1usize..30, - n_recipients in 1..30, + n_outputs in 1..30, ) ( @@ -843,12 +884,12 @@ pub mod testing { arb_positive_note_value(MAX_NOTE_VALUE / n_notes as u64).prop_flat_map(arb_note), n_notes ), - recipient_amounts in vec( + output_amounts in vec( arb_address().prop_flat_map(move |a| { - arb_positive_note_value(MAX_NOTE_VALUE / n_recipients as u64) + arb_positive_note_value(MAX_NOTE_VALUE / n_outputs as u64) .prop_map(move |v| (a, v)) }), - n_recipients as usize + n_outputs as usize ), rng_seed in prop::array::uniform32(prop::num::u8::ANY) ) -> ArbitraryBundleInputs { @@ -873,7 +914,7 @@ pub mod testing { sk, anchor: frontier.root().into(), notes: notes_and_auth_paths, - recipient_amounts + output_amounts } } } @@ -899,6 +940,7 @@ mod tests { use super::Builder; use crate::{ + builder::BundleType, bundle::{Authorized, Bundle, Flags}, circuit::ProvingKey, constants::MERKLE_DEPTH_ORCHARD, @@ -922,13 +964,13 @@ mod tests { ); builder - .add_recipient(None, recipient, NoteValue::from_raw(5000), None) + .add_output(None, recipient, NoteValue::from_raw(5000), None) .unwrap(); let balance: i64 = builder.value_balance().unwrap(); assert_eq!(balance, -5000); let bundle: Bundle = builder - .build(&mut rng) + .build(&mut rng, &BundleType::Transactional) .unwrap() .create_proof(&pk, &mut rng) .unwrap() diff --git a/src/value.rs b/src/value.rs index 2f86c7234..302c0b3ff 100644 --- a/src/value.rs +++ b/src/value.rs @@ -16,7 +16,7 @@ //! - Define your `valueBalanceOrchard` type to enforce your valid value range. This can //! be checked in its `TryFrom` implementation. //! - Define your own "amount" type for note values, and convert it to `NoteValue` prior -//! to calling [`Builder::add_recipient`]. +//! to calling [`Builder::add_output`]. //! //! Inside the circuit, note values are constrained to be unsigned 64-bit integers. //! @@ -34,7 +34,7 @@ //! [`Bundle`]: crate::bundle::Bundle //! [`Bundle::value_balance`]: crate::bundle::Bundle::value_balance //! [`Builder::value_balance`]: crate::builder::Builder::value_balance -//! [`Builder::add_recipient`]: crate::builder::Builder::add_recipient +//! [`Builder::add_output`]: crate::builder::Builder::add_output //! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html use core::fmt::{self, Debug}; diff --git a/tests/builder.rs b/tests/builder.rs index 8cda6e708..40194cd6e 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -1,7 +1,7 @@ use bridgetree::BridgeTree; use incrementalmerkletree::Hashable; use orchard::{ - builder::Builder, + builder::{Builder, BundleType}, bundle::{Authorized, Flags}, circuit::{ProvingKey, VerifyingKey}, keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey, SpendingKey}, @@ -44,10 +44,10 @@ fn bundle_chain() { let mut builder = Builder::new(Flags::from_parts(false, true), anchor); assert_eq!( - builder.add_recipient(None, recipient, NoteValue::from_raw(5000), None), + builder.add_output(None, recipient, NoteValue::from_raw(5000), None), Ok(()) ); - let unauthorized = builder.build(&mut rng).unwrap(); + let unauthorized = builder.build(&mut rng, &BundleType::Transactional).unwrap(); let sighash = unauthorized.commitment().into(); let proven = unauthorized.create_proof(&pk, &mut rng).unwrap(); proven.apply_signatures(rng, sighash, &[]).unwrap() @@ -86,10 +86,10 @@ fn bundle_chain() { let mut builder = Builder::new(Flags::from_parts(true, true), anchor); assert_eq!(builder.add_spend(fvk, note, merkle_path), Ok(())); assert_eq!( - builder.add_recipient(None, recipient, NoteValue::from_raw(5000), None), + builder.add_output(None, recipient, NoteValue::from_raw(5000), None), Ok(()) ); - let unauthorized = builder.build(&mut rng).unwrap(); + let unauthorized = builder.build(&mut rng, &BundleType::Transactional).unwrap(); let sighash = unauthorized.commitment().into(); let proven = unauthorized.create_proof(&pk, &mut rng).unwrap(); proven