diff --git a/CHANGELOG.md b/CHANGELOG.md index 471621c5..bf766fa8 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/benches/circuit.rs b/benches/circuit.rs index 571ce9a0..5d66f142 100644 --- a/benches/circuit.rs +++ b/benches/circuit.rs @@ -31,7 +31,7 @@ fn criterion_benchmark(c: &mut Criterion) { .add_output(None, recipient, NoteValue::from_raw(10), None) .unwrap(); } - let bundle: Bundle<_, i64> = builder.build(rng).unwrap().unwrap(); + let bundle: Bundle<_, i64> = builder.build(rng).unwrap().unwrap().0; let instances: Vec<_> = bundle .actions() diff --git a/benches/note_decryption.rs b/benches/note_decryption.rs index aa10d1de..2cd177e8 100644 --- a/benches/note_decryption.rs +++ b/benches/note_decryption.rs @@ -53,7 +53,7 @@ fn bench_note_decryption(c: &mut Criterion) { builder .add_output(None, recipient, NoteValue::from_raw(10), None) .unwrap(); - let bundle: Bundle<_, i64> = builder.build(rng).unwrap().unwrap(); + let bundle: Bundle<_, i64> = builder.build(rng).unwrap().unwrap().0; bundle .create_proof(&pk, rng) .unwrap() diff --git a/src/builder.rs b/src/builder.rs index d7327569..69ae5b59 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -373,6 +373,56 @@ impl ActionInfo { /// This is returned by [`Builder::build`]. pub type UnauthorizedBundle = Bundle, V>; +/// Metadata about a bundle created by [`bundle`] or [`Builder::build`] that is not +/// necessarily recoverable from the bundle itself. +/// +/// This includes information about how [`Action`]s within the bundle are ordered (after +/// padding and randomization) relative to the order in which spends and outputs were +/// provided (to [`bundle`]), or the order in which [`Builder`] mutations were performed. +#[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 the metadata for a [`Bundle`] that contains only dummy actions, if any. + pub fn empty() -> Self { + Self::new(0, 0) + } + + /// Returns the index within the bundle 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`]. + /// + /// For the purpose of improving indistinguishability, actions are padded and note + /// positions are randomized when building bundles. This means that the bundle + /// consumer cannot assume that e.g. the first spend they added corresponds to the + /// first action in the bundle. + pub fn spend_action_index(&self, n: usize) -> Option { + self.spend_indices.get(n).copied() + } + + /// Returns the index within the bundle 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`]. + /// + /// For the purpose of improving indistinguishability, actions are padded and note + /// positions are randomized when building bundles. This means that the bundle + /// consumer cannot assume that e.g. the first output they added corresponds to the + /// first action in the bundle. + 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 +542,7 @@ impl Builder { pub fn build>( self, rng: impl RngCore, - ) -> Result>, BuildError> { + ) -> Result, BundleMetadata)>, BuildError> { bundle( rng, self.anchor, @@ -511,9 +561,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 +587,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 +661,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 +1031,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 +1144,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 fa35a32f..8ce67e92 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