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.
This commit is contained in:
Kris Nuttycombe 2024-01-09 11:48:15 -07:00
parent e127230af0
commit b60ef5c7ea
5 changed files with 119 additions and 34 deletions

View File

@ -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<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`

View File

@ -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()

View File

@ -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()

View File

@ -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)]
@ -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,
@ -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();
@ -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.
@ -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,
)
}))
}
@ -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])

View File

@ -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