use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::fmt;
use core::iter;
use ff::Field;
use pasta_curves::pallas;
use rand::{prelude::SliceRandom, CryptoRng, RngCore};
use crate::{
address::Address,
bundle::{Authorization, Authorized, Bundle, Flags},
keys::{
FullViewingKey, OutgoingViewingKey, Scope, SpendAuthorizingKey, SpendValidatingKey,
SpendingKey,
},
note::{ExtractedNoteCommitment, Note, Nullifier, Rho, TransmittedNoteCiphertext},
note_encryption::OrchardNoteEncryption,
primitives::redpallas::{self, Binding, SpendAuth},
tree::{Anchor, MerklePath},
value::{self, BalanceError, NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum},
Proof,
};
#[cfg(feature = "circuit")]
use {
crate::{
action::Action,
circuit::{Circuit, Instance, ProvingKey},
},
nonempty::NonEmpty,
};
const MIN_ACTIONS: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BundleType {
Transactional {
flags: Flags,
bundle_required: bool,
},
Coinbase,
}
impl BundleType {
pub const DEFAULT: BundleType = BundleType::Transactional {
flags: Flags::ENABLED,
bundle_required: false,
};
pub const DISABLED: BundleType = BundleType::Transactional {
flags: Flags::from_parts(false, false),
bundle_required: false,
};
pub fn num_actions(
&self,
num_spends: usize,
num_outputs: usize,
) -> Result<usize, &'static str> {
let num_requested_actions = core::cmp::max(num_spends, num_outputs);
match self {
BundleType::Transactional {
flags,
bundle_required,
} => {
if !flags.spends_enabled() && num_spends > 0 {
Err("Spends are disabled, so num_spends must be zero")
} else if !flags.outputs_enabled() && num_outputs > 0 {
Err("Outputs are disabled, so num_outputs must be zero")
} else {
Ok(if *bundle_required || num_requested_actions > 0 {
core::cmp::max(num_requested_actions, MIN_ACTIONS)
} else {
0
})
}
}
BundleType::Coinbase => {
if num_spends > 0 {
Err("Coinbase bundles have spends disabled, so num_spends must be zero")
} else {
Ok(num_outputs)
}
}
}
}
pub fn flags(&self) -> Flags {
match self {
BundleType::Transactional { flags, .. } => *flags,
BundleType::Coinbase => Flags::SPENDS_DISABLED,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum BuildError {
SpendsDisabled,
OutputsDisabled,
AnchorMismatch,
MissingSignatures,
#[cfg(feature = "circuit")]
Proof(halo2_proofs::plonk::Error),
ValueSum(value::BalanceError),
InvalidExternalSignature,
DuplicateSignature,
BundleTypeNotSatisfiable,
}
impl fmt::Display for BuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use BuildError::*;
match self {
MissingSignatures => f.write_str("Required signatures were missing during build"),
#[cfg(feature = "circuit")]
Proof(e) => f.write_str(&format!("Could not create proof: {}", e)),
ValueSum(_) => f.write_str("Overflow occurred during value construction"),
InvalidExternalSignature => f.write_str("External signature was invalid"),
DuplicateSignature => f.write_str("Signature valid for more than one input"),
BundleTypeNotSatisfiable => {
f.write_str("Bundle structure did not conform to requested bundle type.")
}
SpendsDisabled => f.write_str("Spends are not enabled for the requested bundle type."),
OutputsDisabled => f.write_str("Spends are not enabled for the requested bundle type."),
AnchorMismatch => {
f.write_str("All spends must share the anchor requested for the transaction.")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for BuildError {}
#[cfg(feature = "circuit")]
impl From<halo2_proofs::plonk::Error> for BuildError {
fn from(e: halo2_proofs::plonk::Error) -> Self {
BuildError::Proof(e)
}
}
impl From<value::BalanceError> for BuildError {
fn from(e: value::BalanceError) -> Self {
BuildError::ValueSum(e)
}
}
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SpendError {
SpendsDisabled,
AnchorMismatch,
FvkMismatch,
}
impl fmt::Display for SpendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use SpendError::*;
f.write_str(match self {
SpendsDisabled => "Spends are not enabled for this builder",
AnchorMismatch => "All anchors must be equal.",
FvkMismatch => "FullViewingKey does not correspond to the given note",
})
}
}
#[cfg(feature = "std")]
impl std::error::Error for SpendError {}
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum OutputError {
OutputsDisabled,
}
impl fmt::Display for OutputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use OutputError::*;
f.write_str(match self {
OutputsDisabled => "Outputs are not enabled for this builder",
})
}
}
#[cfg(feature = "std")]
impl std::error::Error for OutputError {}
#[derive(Debug)]
pub struct SpendInfo {
pub(crate) dummy_sk: Option<SpendingKey>,
pub(crate) fvk: FullViewingKey,
pub(crate) scope: Scope,
pub(crate) note: Note,
pub(crate) merkle_path: MerklePath,
}
impl SpendInfo {
pub fn new(fvk: FullViewingKey, note: Note, merkle_path: MerklePath) -> Option<Self> {
let scope = fvk.scope_for_address(¬e.recipient())?;
Some(SpendInfo {
dummy_sk: None,
fvk,
scope,
note,
merkle_path,
})
}
fn dummy(rng: &mut impl RngCore) -> Self {
let (sk, fvk, note) = Note::dummy(rng, None);
let merkle_path = MerklePath::dummy(rng);
SpendInfo {
dummy_sk: Some(sk),
fvk,
scope: Scope::External,
note,
merkle_path,
}
}
fn has_matching_anchor(&self, anchor: &Anchor) -> bool {
if self.note.value() == NoteValue::zero() {
true
} else {
let cm = self.note.commitment();
let path_root = self.merkle_path.root(cm.into());
&path_root == anchor
}
}
fn build(
&self,
mut rng: impl RngCore,
) -> (
Nullifier,
SpendValidatingKey,
pallas::Scalar,
redpallas::VerificationKey<SpendAuth>,
) {
let nf_old = self.note.nullifier(&self.fvk);
let ak: SpendValidatingKey = self.fvk.clone().into();
let alpha = pallas::Scalar::random(&mut rng);
let rk = ak.randomize(&alpha);
(nf_old, ak, alpha, rk)
}
fn into_pczt(self, rng: impl RngCore) -> crate::pczt::Spend {
let (nf_old, _, alpha, rk) = self.build(rng);
crate::pczt::Spend {
nullifier: nf_old,
rk,
spend_auth_sig: None,
recipient: Some(self.note.recipient()),
value: Some(self.note.value()),
rho: Some(self.note.rho()),
rseed: Some(*self.note.rseed()),
fvk: Some(self.fvk),
witness: Some(self.merkle_path),
alpha: Some(alpha),
zip32_derivation: None,
dummy_sk: self.dummy_sk,
proprietary: BTreeMap::new(),
}
}
}
#[derive(Debug)]
pub struct OutputInfo {
ovk: Option<OutgoingViewingKey>,
recipient: Address,
value: NoteValue,
memo: [u8; 512],
}
impl OutputInfo {
pub fn new(
ovk: Option<OutgoingViewingKey>,
recipient: Address,
value: NoteValue,
memo: [u8; 512],
) -> Self {
Self {
ovk,
recipient,
value,
memo,
}
}
pub fn dummy(rng: &mut impl RngCore) -> Self {
let fvk: FullViewingKey = (&SpendingKey::random(rng)).into();
let recipient = fvk.address_at(0u32, Scope::External);
Self::new(None, recipient, NoteValue::zero(), [0u8; 512])
}
fn build(
&self,
cv_net: &ValueCommitment,
nf_old: Nullifier,
mut rng: impl RngCore,
) -> (Note, ExtractedNoteCommitment, TransmittedNoteCiphertext) {
let rho = Rho::from_nf_old(nf_old);
let note = Note::new(self.recipient, self.value, rho, &mut rng);
let cm_new = note.commitment();
let cmx = cm_new.into();
let encryptor = OrchardNoteEncryption::new(self.ovk.clone(), note, self.memo);
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: encryptor.epk().to_bytes().0,
enc_ciphertext: encryptor.encrypt_note_plaintext(),
out_ciphertext: encryptor.encrypt_outgoing_plaintext(cv_net, &cmx, &mut rng),
};
(note, cmx, encrypted_note)
}
fn into_pczt(
self,
cv_net: &ValueCommitment,
nf_old: Nullifier,
rng: impl RngCore,
) -> crate::pczt::Output {
let (note, cmx, encrypted_note) = self.build(cv_net, nf_old, rng);
crate::pczt::Output {
cmx,
encrypted_note,
recipient: Some(self.recipient),
value: Some(self.value),
rseed: Some(*note.rseed()),
ock: None,
zip32_derivation: None,
user_address: None,
proprietary: BTreeMap::new(),
}
}
}
#[derive(Debug)]
struct ActionInfo {
spend: SpendInfo,
output: OutputInfo,
rcv: ValueCommitTrapdoor,
}
impl ActionInfo {
fn new(spend: SpendInfo, output: OutputInfo, rng: impl RngCore) -> Self {
ActionInfo {
spend,
output,
rcv: ValueCommitTrapdoor::random(rng),
}
}
fn value_sum(&self) -> ValueSum {
self.spend.note.value() - self.output.value
}
#[cfg(feature = "circuit")]
fn build(self, mut rng: impl RngCore) -> (Action<SigningMetadata>, Circuit) {
let v_net = self.value_sum();
let cv_net = ValueCommitment::derive(v_net, self.rcv.clone());
let (nf_old, ak, alpha, rk) = self.spend.build(&mut rng);
let (note, cmx, encrypted_note) = self.output.build(&cv_net, nf_old, &mut rng);
(
Action::from_parts(
nf_old,
rk,
cmx,
encrypted_note,
cv_net,
SigningMetadata {
dummy_ask: self.spend.dummy_sk.as_ref().map(SpendAuthorizingKey::from),
parts: SigningParts { ak, alpha },
},
),
Circuit::from_action_context_unchecked(self.spend, note, alpha, self.rcv),
)
}
fn build_for_pczt(self, mut rng: impl RngCore) -> crate::pczt::Action {
let v_net = self.value_sum();
let cv_net = ValueCommitment::derive(v_net, self.rcv.clone());
let spend = self.spend.into_pczt(&mut rng);
let output = self.output.into_pczt(&cv_net, spend.nullifier, &mut rng);
crate::pczt::Action {
cv_net,
spend,
output,
rcv: Some(self.rcv),
}
}
}
#[cfg(feature = "circuit")]
pub type UnauthorizedBundle<V> = Bundle<InProgress<Unproven, Unauthorized>, V>;
#[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],
}
}
pub fn empty() -> Self {
Self::new(0, 0)
}
pub fn spend_action_index(&self, n: usize) -> Option<usize> {
self.spend_indices.get(n).copied()
}
pub fn output_action_index(&self, n: usize) -> Option<usize> {
self.output_indices.get(n).copied()
}
}
#[derive(Debug)]
pub struct Builder {
spends: Vec<SpendInfo>,
outputs: Vec<OutputInfo>,
bundle_type: BundleType,
anchor: Anchor,
}
impl Builder {
pub fn new(bundle_type: BundleType, anchor: Anchor) -> Self {
Builder {
spends: vec![],
outputs: vec![],
bundle_type,
anchor,
}
}
pub fn add_spend(
&mut self,
fvk: FullViewingKey,
note: Note,
merkle_path: MerklePath,
) -> Result<(), SpendError> {
let flags = self.bundle_type.flags();
if !flags.spends_enabled() {
return Err(SpendError::SpendsDisabled);
}
let spend = SpendInfo::new(fvk, note, merkle_path).ok_or(SpendError::FvkMismatch)?;
if !spend.has_matching_anchor(&self.anchor) {
return Err(SpendError::AnchorMismatch);
}
self.spends.push(spend);
Ok(())
}
pub fn add_output(
&mut self,
ovk: Option<OutgoingViewingKey>,
recipient: Address,
value: NoteValue,
memo: [u8; 512],
) -> Result<(), OutputError> {
let flags = self.bundle_type.flags();
if !flags.outputs_enabled() {
return Err(OutputError::OutputsDisabled);
}
self.outputs
.push(OutputInfo::new(ovk, recipient, value, memo));
Ok(())
}
pub fn spends(&self) -> &Vec<impl InputView<()>> {
&self.spends
}
pub fn outputs(&self) -> &Vec<impl OutputView> {
&self.outputs
}
pub fn value_balance<V: TryFrom<i64>>(&self) -> Result<V, value::BalanceError> {
let value_balance = self
.spends
.iter()
.map(|spend| spend.note.value() - NoteValue::zero())
.chain(
self.outputs
.iter()
.map(|output| NoteValue::zero() - output.value),
)
.try_fold(ValueSum::zero(), |acc, note_value| acc + note_value)
.ok_or(BalanceError::Overflow)?;
i64::try_from(value_balance)
.and_then(|i| V::try_from(i).map_err(|_| value::BalanceError::Overflow))
}
#[cfg(feature = "circuit")]
pub fn build<V: TryFrom<i64>>(
self,
rng: impl RngCore,
) -> Result<Option<(UnauthorizedBundle<V>, BundleMetadata)>, BuildError> {
bundle(
rng,
self.anchor,
self.bundle_type,
self.spends,
self.outputs,
)
}
pub fn build_for_pczt(
self,
rng: impl RngCore,
) -> Result<(crate::pczt::Bundle, BundleMetadata), BuildError> {
build_bundle(
rng,
self.anchor,
self.bundle_type,
self.spends,
self.outputs,
|pre_actions, flags, value_sum, bundle_meta, mut rng| {
let actions = pre_actions
.into_iter()
.map(|a| a.build_for_pczt(&mut rng))
.collect::<Vec<_>>();
Ok((
crate::pczt::Bundle {
actions,
flags,
value_sum,
anchor: self.anchor,
zkproof: None,
bsk: None,
},
bundle_meta,
))
},
)
}
}
#[cfg(feature = "circuit")]
pub fn bundle<V: TryFrom<i64>>(
rng: impl RngCore,
anchor: Anchor,
bundle_type: BundleType,
spends: Vec<SpendInfo>,
outputs: Vec<OutputInfo>,
) -> Result<Option<(UnauthorizedBundle<V>, BundleMetadata)>, BuildError> {
build_bundle(
rng,
anchor,
bundle_type,
spends,
outputs,
|pre_actions, flags, value_balance, bundle_meta, mut rng| {
let result_value_balance: V = i64::try_from(value_balance)
.map_err(BuildError::ValueSum)
.and_then(|i| {
V::try_from(i).map_err(|_| BuildError::ValueSum(value::BalanceError::Overflow))
})?;
let bsk = pre_actions
.iter()
.map(|a| &a.rcv)
.sum::<ValueCommitTrapdoor>()
.into_bsk();
let (actions, circuits): (Vec<_>, Vec<_>) =
pre_actions.into_iter().map(|a| a.build(&mut rng)).unzip();
let bvk = (actions.iter().map(|a| a.cv_net()).sum::<ValueCommitment>()
- ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero()))
.into_bvk();
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_meta,
)
}))
},
)
}
fn build_bundle<B, R: RngCore>(
mut rng: R,
anchor: Anchor,
bundle_type: BundleType,
spends: Vec<SpendInfo>,
outputs: Vec<OutputInfo>,
finisher: impl FnOnce(Vec<ActionInfo>, Flags, ValueSum, BundleMetadata, R) -> Result<B, BuildError>,
) -> Result<B, BuildError> {
let flags = bundle_type.flags();
let num_requested_spends = spends.len();
if !flags.spends_enabled() && num_requested_spends > 0 {
return Err(BuildError::SpendsDisabled);
}
for spend in &spends {
if !spend.has_matching_anchor(&anchor) {
return Err(BuildError::AnchorMismatch);
}
}
let num_requested_outputs = outputs.len();
if !flags.outputs_enabled() && num_requested_outputs > 0 {
return Err(BuildError::OutputsDisabled);
}
let num_actions = bundle_type
.num_actions(num_requested_spends, num_requested_outputs)
.map_err(|_| BuildError::BundleTypeNotSatisfiable)?;
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<_>>();
indexed_spends.shuffle(&mut rng);
indexed_outputs.shuffle(&mut rng);
let mut bundle_meta = BundleMetadata::new(num_requested_spends, num_requested_outputs);
let pre_actions = indexed_spends
.into_iter()
.zip(indexed_outputs)
.enumerate()
.map(|(action_idx, ((spend_idx, spend), (out_idx, output)))| {
if spend_idx < num_requested_spends {
bundle_meta.spend_indices[spend_idx] = action_idx;
}
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)
};
let value_balance = pre_actions
.iter()
.try_fold(ValueSum::zero(), |acc, action| acc + action.value_sum())
.ok_or(BalanceError::Overflow)?;
finisher(pre_actions, flags, value_balance, bundle_meta, rng)
}
pub trait InProgressSignatures: fmt::Debug {
type SpendAuth: fmt::Debug;
}
#[derive(Clone, Debug)]
pub struct InProgress<P, S: InProgressSignatures> {
proof: P,
sigs: S,
}
impl<P: fmt::Debug, S: InProgressSignatures> Authorization for InProgress<P, S> {
type SpendAuth = S::SpendAuth;
}
#[cfg(feature = "circuit")]
#[derive(Clone, Debug)]
pub struct Unproven {
circuits: Vec<Circuit>,
}
#[cfg(feature = "circuit")]
impl<S: InProgressSignatures> InProgress<Unproven, S> {
pub fn create_proof(
&self,
pk: &ProvingKey,
instances: &[Instance],
rng: impl RngCore,
) -> Result<Proof, halo2_proofs::plonk::Error> {
Proof::create(pk, &self.proof.circuits, instances, rng)
}
}
#[cfg(feature = "circuit")]
impl<S: InProgressSignatures, V> Bundle<InProgress<Unproven, S>, V> {
pub fn create_proof(
self,
pk: &ProvingKey,
mut rng: impl RngCore,
) -> Result<Bundle<InProgress<Proof, S>, V>, BuildError> {
let instances: Vec<_> = self
.actions()
.iter()
.map(|a| a.to_instance(*self.flags(), *self.anchor()))
.collect();
self.try_map_authorization(
&mut (),
|_, _, a| Ok(a),
|_, auth| {
let proof = auth.create_proof(pk, &instances, &mut rng)?;
Ok(InProgress {
proof,
sigs: auth.sigs,
})
},
)
}
}
#[derive(Clone, Debug)]
pub struct SigningParts {
ak: SpendValidatingKey,
alpha: pallas::Scalar,
}
#[derive(Clone, Debug)]
pub struct Unauthorized {
bsk: redpallas::SigningKey<Binding>,
}
impl InProgressSignatures for Unauthorized {
type SpendAuth = SigningMetadata;
}
#[derive(Clone, Debug)]
pub struct SigningMetadata {
dummy_ask: Option<SpendAuthorizingKey>,
parts: SigningParts,
}
#[derive(Debug)]
pub struct PartiallyAuthorized {
binding_signature: redpallas::Signature<Binding>,
sighash: [u8; 32],
}
impl InProgressSignatures for PartiallyAuthorized {
type SpendAuth = MaybeSigned;
}
#[derive(Debug)]
pub enum MaybeSigned {
SigningMetadata(SigningParts),
Signature(redpallas::Signature<SpendAuth>),
}
impl MaybeSigned {
fn finalize(self) -> Result<redpallas::Signature<SpendAuth>, BuildError> {
match self {
Self::Signature(sig) => Ok(sig),
_ => Err(BuildError::MissingSignatures),
}
}
}
impl<P: fmt::Debug, V> Bundle<InProgress<P, Unauthorized>, V> {
pub fn prepare<R: RngCore + CryptoRng>(
self,
mut rng: R,
sighash: [u8; 32],
) -> Bundle<InProgress<P, PartiallyAuthorized>, V> {
self.map_authorization(
&mut rng,
|rng, _, SigningMetadata { dummy_ask, parts }| {
dummy_ask
.map(|ask| ask.randomize(&parts.alpha).sign(rng, &sighash))
.map(MaybeSigned::Signature)
.unwrap_or(MaybeSigned::SigningMetadata(parts))
},
|rng, auth| InProgress {
proof: auth.proof,
sigs: PartiallyAuthorized {
binding_signature: auth.sigs.bsk.sign(rng, &sighash),
sighash,
},
},
)
}
}
impl<V> Bundle<InProgress<Proof, Unauthorized>, V> {
pub fn apply_signatures<R: RngCore + CryptoRng>(
self,
mut rng: R,
sighash: [u8; 32],
signing_keys: &[SpendAuthorizingKey],
) -> Result<Bundle<Authorized, V>, BuildError> {
signing_keys
.iter()
.fold(self.prepare(&mut rng, sighash), |partial, ask| {
partial.sign(&mut rng, ask)
})
.finalize()
}
}
impl<P: fmt::Debug, V> Bundle<InProgress<P, PartiallyAuthorized>, V> {
pub fn sign<R: RngCore + CryptoRng>(self, mut rng: R, ask: &SpendAuthorizingKey) -> Self {
let expected_ak = ask.into();
self.map_authorization(
&mut rng,
|rng, partial, maybe| match maybe {
MaybeSigned::SigningMetadata(parts) if parts.ak == expected_ak => {
MaybeSigned::Signature(
ask.randomize(&parts.alpha).sign(rng, &partial.sigs.sighash),
)
}
s => s,
},
|_, partial| partial,
)
}
pub fn append_signatures(
self,
signatures: &[redpallas::Signature<SpendAuth>],
) -> Result<Self, BuildError> {
signatures.iter().try_fold(self, Self::append_signature)
}
fn append_signature(
self,
signature: &redpallas::Signature<SpendAuth>,
) -> Result<Self, BuildError> {
let mut signature_valid_for = 0usize;
let bundle = self.map_authorization(
&mut signature_valid_for,
|valid_for, partial, maybe| match maybe {
MaybeSigned::SigningMetadata(parts) => {
let rk = parts.ak.randomize(&parts.alpha);
if rk.verify(&partial.sigs.sighash[..], signature).is_ok() {
*valid_for += 1;
MaybeSigned::Signature(signature.clone())
} else {
MaybeSigned::SigningMetadata(parts)
}
}
s => s,
},
|_, partial| partial,
);
match signature_valid_for {
0 => Err(BuildError::InvalidExternalSignature),
1 => Ok(bundle),
_ => Err(BuildError::DuplicateSignature),
}
}
}
impl<V> Bundle<InProgress<Proof, PartiallyAuthorized>, V> {
pub fn finalize(self) -> Result<Bundle<Authorized, V>, BuildError> {
self.try_map_authorization(
&mut (),
|_, _, maybe| maybe.finalize(),
|_, partial| {
Ok(Authorized::from_parts(
partial.proof,
partial.sigs.binding_signature,
))
},
)
}
}
pub trait InputView<NoteRef> {
fn note_id(&self) -> &NoteRef;
fn value<V: From<u64>>(&self) -> V;
}
impl InputView<()> for SpendInfo {
fn note_id(&self) -> &() {
&()
}
fn value<V: From<u64>>(&self) -> V {
V::from(self.note.value().inner())
}
}
pub trait OutputView {
fn value<V: From<u64>>(&self) -> V;
}
impl OutputView for OutputInfo {
fn value<V: From<u64>>(&self) -> V {
V::from(self.value.inner())
}
}
#[cfg(all(feature = "circuit", any(test, feature = "test-dependencies")))]
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use alloc::vec::Vec;
use core::fmt::Debug;
use incrementalmerkletree::{frontier::Frontier, Hashable};
use rand::{rngs::StdRng, CryptoRng, SeedableRng};
use proptest::collection::vec;
use proptest::prelude::*;
use crate::{
address::testing::arb_address,
bundle::{Authorized, Bundle},
circuit::ProvingKey,
keys::{testing::arb_spending_key, FullViewingKey, SpendAuthorizingKey, SpendingKey},
note::testing::arb_note,
tree::{Anchor, MerkleHashOrchard, MerklePath},
value::{testing::arb_positive_note_value, NoteValue, MAX_NOTE_VALUE},
Address, Note,
};
use super::{Builder, BundleType};
#[derive(Debug)]
struct ArbitraryBundleInputs<R> {
rng: R,
sk: SpendingKey,
anchor: Anchor,
notes: Vec<(Note, MerklePath)>,
output_amounts: Vec<(Address, NoteValue)>,
}
impl<R: RngCore + CryptoRng> ArbitraryBundleInputs<R> {
fn into_bundle<V: TryFrom<i64>>(mut self) -> Bundle<Authorized, V> {
let fvk = FullViewingKey::from(&self.sk);
let mut builder = Builder::new(BundleType::DEFAULT, self.anchor);
for (note, path) in self.notes.into_iter() {
builder.add_spend(fvk.clone(), note, path).unwrap();
}
for (addr, value) in self.output_amounts.into_iter() {
let scope = fvk.scope_for_address(&addr).unwrap();
let ovk = fvk.to_ovk(scope);
builder
.add_output(Some(ovk.clone()), addr, value, [0u8; 512])
.unwrap();
}
let pk = ProvingKey::build();
builder
.build(&mut self.rng)
.unwrap()
.unwrap()
.0
.create_proof(&pk, &mut self.rng)
.unwrap()
.prepare(&mut self.rng, [0; 32])
.sign(&mut self.rng, &SpendAuthorizingKey::from(&self.sk))
.finalize()
.unwrap()
}
}
prop_compose! {
fn arb_bundle_inputs(sk: SpendingKey)
(
n_notes in 1usize..30,
n_outputs in 1..30,
)
(
notes in vec(
arb_positive_note_value(MAX_NOTE_VALUE / n_notes as u64).prop_flat_map(arb_note),
n_notes
),
output_amounts in vec(
arb_address().prop_flat_map(move |a| {
arb_positive_note_value(MAX_NOTE_VALUE / n_outputs as u64)
.prop_map(move |v| (a, v))
}),
n_outputs as usize
),
rng_seed in prop::array::uniform32(prop::num::u8::ANY)
) -> ArbitraryBundleInputs<StdRng> {
use crate::constants::MERKLE_DEPTH_ORCHARD;
let mut frontier = Frontier::<MerkleHashOrchard, { MERKLE_DEPTH_ORCHARD as u8 }>::empty();
let mut notes_and_auth_paths: Vec<(Note, MerklePath)> = Vec::new();
for note in notes.iter() {
let leaf = MerkleHashOrchard::from_cmx(¬e.commitment().into());
frontier.append(leaf);
let path = frontier
.witness(|addr| Some(<MerkleHashOrchard as Hashable>::empty_root(addr.level())))
.ok()
.flatten()
.expect("we can always construct a correct Merkle path");
notes_and_auth_paths.push((*note, path.into()));
}
ArbitraryBundleInputs {
rng: StdRng::from_seed(rng_seed),
sk,
anchor: frontier.root().into(),
notes: notes_and_auth_paths,
output_amounts
}
}
}
pub fn arb_bundle<V: TryFrom<i64> + Debug>() -> impl Strategy<Value = Bundle<Authorized, V>> {
arb_spending_key()
.prop_flat_map(arb_bundle_inputs)
.prop_map(|inputs| inputs.into_bundle::<V>())
}
pub fn arb_bundle_with_key<V: TryFrom<i64> + Debug>(
k: SpendingKey,
) -> impl Strategy<Value = Bundle<Authorized, V>> {
arb_bundle_inputs(k).prop_map(|inputs| inputs.into_bundle::<V>())
}
}
#[cfg(all(test, feature = "circuit"))]
mod tests {
use rand::rngs::OsRng;
use super::Builder;
use crate::{
builder::BundleType,
bundle::{Authorized, Bundle},
circuit::ProvingKey,
constants::MERKLE_DEPTH_ORCHARD,
keys::{FullViewingKey, Scope, SpendingKey},
tree::EMPTY_ROOTS,
value::NoteValue,
};
#[test]
fn shielding_bundle() {
let pk = ProvingKey::build();
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32, Scope::External);
let mut builder = Builder::new(
BundleType::DEFAULT,
EMPTY_ROOTS[MERKLE_DEPTH_ORCHARD].into(),
);
builder
.add_output(None, recipient, NoteValue::from_raw(5000), [0u8; 512])
.unwrap();
let balance: i64 = builder.value_balance().unwrap();
assert_eq!(balance, -5000);
let bundle: Bundle<Authorized, i64> = builder
.build(&mut rng)
.unwrap()
.unwrap()
.0
.create_proof(&pk, &mut rng)
.unwrap()
.prepare(rng, [0; 32])
.finalize()
.unwrap();
assert_eq!(bundle.value_balance(), &(-5000))
}
}