diff --git a/CHANGELOG.md b/CHANGELOG.md index 471621c51..bf766fa8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to Rust's notion of ### Added - `orchard::builder::bundle` +- `orchard::builder::BundleMetadata` - `orchard::builder::BundleType` - `orchard::builder::OutputInfo` - `orchard::bundle::Flags::{ENABLED, SPENDS_DISABLED, OUTPUTS_DISABLED}` @@ -29,7 +30,7 @@ and this project adheres to Rust's notion of 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. - It also now returns a `Result>, ...>` instead of a + It also now returns a `Result, BundleMetadata)>, ...>` instead of a `Result, ...>`. - `orchard::builder::BuildError` has additional variants: - `SpendsDisabled` diff --git a/src/builder.rs b/src/builder.rs index d73275690..ef9544920 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -373,6 +373,50 @@ impl ActionInfo { /// This is returned by [`Builder::build`]. pub type UnauthorizedBundle = Bundle, V>; +/// Metadata about a how a transaction created by a [`bundle`] ordered actions relative to the +/// order in which spends and outputs were provided +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BundleMetadata { + spend_indices: Vec, + output_indices: Vec, +} + +impl BundleMetadata { + fn new(num_requested_spends: usize, num_requested_outputs: usize) -> Self { + BundleMetadata { + spend_indices: vec![0; num_requested_spends], + output_indices: vec![0; num_requested_outputs], + } + } + + /// Returns a new empty [`BundleMetadata`]. + pub fn empty() -> Self { + Self::new(0, 0) + } + + /// Returns the index within the transaction of the [`Action`] corresponding to the `n`-th + /// spend specified in bundle construction. If a [`Builder`] was used, this refers to the spend + /// added by the `n`-th call to [`Builder::add_spend`]. + /// + /// Note positions are randomized when building transactions for indistinguishability. + /// This means that the transaction consumer cannot assume that e.g. the first spend + /// they added corresponds to the first [`Action`] in the transaction. + pub fn spend_action_index(&self, n: usize) -> Option { + self.spend_indices.get(n).copied() + } + + /// Returns the index within the transaction of the [`Action`] corresponding to the `n`-th + /// output specified in bundle construction. If a [`Builder`] was used, this refers to the + /// output added by the `n`-th call to [`Builder::add_output`]. + /// + /// Note positions are randomized when building transactions for indistinguishability. + /// This means that the transaction consumer cannot assume that e.g. the first output + /// they added corresponds to the first [`Action`] in the transaction. + pub fn output_action_index(&self, n: usize) -> Option { + self.output_indices.get(n).copied() + } +} + /// A builder that constructs a [`Bundle`] from a set of notes to be spent, and outputs /// to receive funds. #[derive(Debug)] @@ -492,7 +536,7 @@ impl Builder { pub fn build>( self, rng: impl RngCore, - ) -> Result>, BuildError> { + ) -> Result, BundleMetadata)>, BuildError> { bundle( rng, self.anchor, @@ -511,9 +555,9 @@ pub fn bundle>( mut rng: impl RngCore, anchor: Anchor, bundle_type: BundleType, - mut spends: Vec, - mut outputs: Vec, -) -> Result>, BuildError> { + spends: Vec, + outputs: Vec, +) -> Result, BundleMetadata)>, BuildError> { let flags = bundle_type.flags(); let num_requested_spends = spends.len(); @@ -537,27 +581,48 @@ pub fn bundle>( .map_err(|_| BuildError::BundleTypeNotSatisfiable)?; // Pair up the spends and outputs, extending with dummy values as necessary. - let pre_actions: Vec<_> = { - spends.extend( - iter::repeat_with(|| SpendInfo::dummy(&mut rng)) - .take(num_actions - num_requested_spends), - ); - outputs.extend( - iter::repeat_with(|| OutputInfo::dummy(&mut rng)) - .take(num_actions - num_requested_outputs), - ); + let (pre_actions, bundle_meta) = { + let mut indexed_spends = spends + .into_iter() + .chain(iter::repeat_with(|| SpendInfo::dummy(&mut rng))) + .enumerate() + .take(num_actions) + .collect::>(); + + let mut indexed_outputs = outputs + .into_iter() + .chain(iter::repeat_with(|| OutputInfo::dummy(&mut rng))) + .enumerate() + .take(num_actions) + .collect::>(); // 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. - spends.shuffle(&mut rng); - outputs.shuffle(&mut rng); + indexed_spends.shuffle(&mut rng); + indexed_outputs.shuffle(&mut rng); - spends + let mut bundle_meta = BundleMetadata::new(num_requested_spends, num_requested_outputs); + let pre_actions = indexed_spends .into_iter() - .zip(outputs.into_iter()) - .map(|(spend, output)| ActionInfo::new(spend, output, &mut rng)) - .collect() + .zip(indexed_outputs.into_iter()) + .enumerate() + .map(|(action_idx, ((spend_idx, spend), (out_idx, output)))| { + // Record the post-randomization spend location + if spend_idx < num_requested_spends { + bundle_meta.spend_indices[spend_idx] = action_idx; + } + + // Record the post-randomization output location + if out_idx < num_requested_outputs { + bundle_meta.output_indices[out_idx] = action_idx; + } + + ActionInfo::new(spend, output, &mut rng) + }) + .collect::>(); + + (pre_actions, bundle_meta) }; // Determine the value balance for this bundle, ensuring it is valid. @@ -590,15 +655,18 @@ pub fn bundle>( assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); Ok(NonEmpty::from_vec(actions).map(|actions| { - Bundle::from_parts( - actions, - flags, - result_value_balance, - anchor, - InProgress { - proof: Unproven { circuits }, - sigs: Unauthorized { bsk }, - }, + ( + Bundle::from_parts( + actions, + flags, + result_value_balance, + anchor, + InProgress { + proof: Unproven { circuits }, + sigs: Unauthorized { bsk }, + }, + ), + bundle_meta, ) })) } @@ -957,6 +1025,7 @@ pub mod testing { .build(&mut self.rng) .unwrap() .unwrap() + .0 .create_proof(&pk, &mut self.rng) .unwrap() .prepare(&mut self.rng, [0; 32]) @@ -1069,6 +1138,7 @@ mod tests { .build(&mut rng) .unwrap() .unwrap() + .0 .create_proof(&pk, &mut rng) .unwrap() .prepare(rng, [0; 32]) diff --git a/tests/builder.rs b/tests/builder.rs index fa35a32fe..8ce67e92a 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -49,11 +49,25 @@ fn bundle_chain() { }, anchor, ); + let note_value = NoteValue::from_raw(5000); assert_eq!( - builder.add_output(None, recipient, NoteValue::from_raw(5000), None), + builder.add_output(None, recipient, note_value, None), Ok(()) ); - let unauthorized = builder.build(&mut rng).unwrap().unwrap(); + let (unauthorized, bundle_meta) = builder.build(&mut rng).unwrap().unwrap(); + + assert_eq!( + unauthorized + .decrypt_output_with_key( + bundle_meta + .output_action_index(0) + .expect("Output 0 can be found"), + &fvk.to_ivk(Scope::External) + ) + .map(|(note, _, _)| note.value()), + Some(note_value) + ); + let sighash = unauthorized.commitment().into(); let proven = unauthorized.create_proof(&pk, &mut rng).unwrap(); proven.apply_signatures(rng, sighash, &[]).unwrap() @@ -95,7 +109,7 @@ fn bundle_chain() { builder.add_output(None, recipient, NoteValue::from_raw(5000), None), Ok(()) ); - let unauthorized = builder.build(&mut rng).unwrap().unwrap(); + let (unauthorized, _) = builder.build(&mut rng).unwrap().unwrap(); let sighash = unauthorized.commitment().into(); let proven = unauthorized.create_proof(&pk, &mut rng).unwrap(); proven