From a62cd558e62ae9d561096fa68c45820a63f0ce37 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 9 Jan 2024 11:48:15 -0700 Subject: [PATCH] Return bundle metadata from bundle building. In order to be able to associate requested spends and outputs with the indices of the actions that execute these operations, it is necessary to track the randomization of inputs and outputs and return the mappings that resulted from that shuffling. --- CHANGELOG.md | 3 +- src/builder.rs | 126 ++++++++++++++++++++++++++++++++++++----------- tests/builder.rs | 20 ++++++-- 3 files changed, 117 insertions(+), 32 deletions(-) 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