Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return bundle metadata from bundle building. #412

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion benches/circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion benches/note_decryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
132 changes: 104 additions & 28 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,56 @@ impl ActionInfo {
/// This is returned by [`Builder::build`].
pub type UnauthorizedBundle<V> = Bundle<InProgress<Unproven, Unauthorized>, 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<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 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<usize> {
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<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 +542,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 +561,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 +587,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 +661,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 +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])
Expand Down Expand Up @@ -1069,6 +1144,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
Loading