Skip to content

Commit

Permalink
Return bundle metadata from bundle building.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nuttycom committed Jan 9, 2024
1 parent e127230 commit a62cd55
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 32 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand All @@ -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<Option<Bundle<...>>, ...>` instead of a
It also now returns a `Result<Option<(Bundle<...>, BundleMetadata)>, ...>` instead of a
`Result<Bundle<...>, ...>`.
- `orchard::builder::BuildError` has additional variants:
- `SpendsDisabled`
Expand Down
126 changes: 98 additions & 28 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,50 @@ impl ActionInfo {
/// This is returned by [`Builder::build`].
pub type UnauthorizedBundle<V> = Bundle<InProgress<Unproven, Unauthorized>, 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<usize>,
output_indices: Vec<usize>,
}

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<usize> {
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<usize> {
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)]
Expand Down Expand Up @@ -492,7 +536,7 @@ impl Builder {
pub fn build<V: TryFrom<i64>>(
self,
rng: impl RngCore,
) -> Result<Option<UnauthorizedBundle<V>>, BuildError> {
) -> Result<Option<(UnauthorizedBundle<V>, BundleMetadata)>, BuildError> {
bundle(
rng,
self.anchor,
Expand All @@ -511,9 +555,9 @@ pub fn bundle<V: TryFrom<i64>>(
mut rng: impl RngCore,
anchor: Anchor,
bundle_type: BundleType,
mut spends: Vec<SpendInfo>,
mut outputs: Vec<OutputInfo>,
) -> Result<Option<UnauthorizedBundle<V>>, BuildError> {
spends: Vec<SpendInfo>,
outputs: Vec<OutputInfo>,
) -> Result<Option<(UnauthorizedBundle<V>, BundleMetadata)>, BuildError> {
let flags = bundle_type.flags();

let num_requested_spends = spends.len();
Expand All @@ -537,27 +581,48 @@ pub fn bundle<V: TryFrom<i64>>(
.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::<Vec<_>>();

let mut indexed_outputs = outputs
.into_iter()
.chain(iter::repeat_with(|| OutputInfo::dummy(&mut rng)))
.enumerate()
.take(num_actions)
.collect::<Vec<_>>();

// 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::<Vec<_>>();

(pre_actions, bundle_meta)
};

// Determine the value balance for this bundle, ensuring it is valid.
Expand Down Expand Up @@ -590,15 +655,18 @@ pub fn bundle<V: TryFrom<i64>>(
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,
)
}))
}
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -1069,6 +1138,7 @@ mod tests {
.build(&mut rng)
.unwrap()
.unwrap()
.0
.create_proof(&pk, &mut rng)
.unwrap()
.prepare(rng, [0; 32])
Expand Down
20 changes: 17 additions & 3 deletions tests/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a62cd55

Please sign in to comment.