From 48d65ee7067da96e9f81cb583f4fa7af8edb855c Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 8 Jun 2022 15:30:40 +0300 Subject: [PATCH] added note_type to builder --- .gitignore | 3 + benches/circuit.rs | 9 ++- benches/note_decryption.rs | 17 +++- src/action.rs | 8 +- src/builder.rs | 156 ++++++++++++++++++++++++++++++------- src/circuit.rs | 7 +- src/note.rs | 5 +- src/note/note_type.rs | 24 +++++- src/tree.rs | 2 +- src/value.rs | 77 ++++++++++++++---- tests/builder.rs | 17 +++- 11 files changed, 269 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 173b9514..d7f36bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ **/*.rs.bk Cargo.lock .vscode +.idea +action-circuit-layout.png +proptest-regressions/*.txt diff --git a/benches/circuit.rs b/benches/circuit.rs index 3140a90a..6400513e 100644 --- a/benches/circuit.rs +++ b/benches/circuit.rs @@ -6,6 +6,7 @@ use criterion::{BenchmarkId, Criterion}; #[cfg(unix)] use pprof::criterion::{Output, PProfProfiler}; +use orchard::note::NoteType; use orchard::{ builder::Builder, bundle::Flags, @@ -32,7 +33,13 @@ fn criterion_benchmark(c: &mut Criterion) { ); for _ in 0..num_recipients { builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(10), + NoteType::native(), + None, + ) .unwrap(); } let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); diff --git a/benches/note_decryption.rs b/benches/note_decryption.rs index cab432fe..73d4fcae 100644 --- a/benches/note_decryption.rs +++ b/benches/note_decryption.rs @@ -4,6 +4,7 @@ use orchard::{ bundle::Flags, circuit::ProvingKey, keys::{FullViewingKey, Scope, SpendingKey}, + note::NoteType, note_encryption::{CompactAction, OrchardDomain}, value::NoteValue, Anchor, Bundle, @@ -51,10 +52,22 @@ fn bench_note_decryption(c: &mut Criterion) { // The builder pads to two actions, and shuffles their order. Add two recipients // so the first action is always decryptable. builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(10), + NoteType::native(), + None, + ) .unwrap(); builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(10), + NoteType::native(), + None, + ) .unwrap(); let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); bundle diff --git a/src/action.rs b/src/action.rs index b6396ed9..b1eda481 100644 --- a/src/action.rs +++ b/src/action.rs @@ -126,7 +126,7 @@ pub(crate) mod testing { use proptest::prelude::*; - use crate::note::NoteType; + use crate::note::note_type::testing::arb_note_type; use crate::{ note::{ commitment::ExtractedNoteCommitment, nullifier::testing::arb_nullifier, @@ -147,12 +147,13 @@ pub(crate) mod testing { nf in arb_nullifier(), rk in arb_spendauth_verification_key(), note in arb_note(output_value), + note_type in arb_note_type() ) -> Action<()> { let cmx = ExtractedNoteCommitment::from(note.commitment()); let cv_net = ValueCommitment::derive( spend_value - output_value, ValueCommitTrapdoor::zero(), - NoteType::native() + note_type ); // FIXME: make a real one from the note. let encrypted_note = TransmittedNoteCiphertext { @@ -179,12 +180,13 @@ pub(crate) mod testing { note in arb_note(output_value), rng_seed in prop::array::uniform32(prop::num::u8::ANY), fake_sighash in prop::array::uniform32(prop::num::u8::ANY), + note_type in arb_note_type() ) -> Action> { let cmx = ExtractedNoteCommitment::from(note.commitment()); let cv_net = ValueCommitment::derive( spend_value - output_value, ValueCommitTrapdoor::zero(), - NoteType::native() + note_type ); // FIXME: make a real one from the note. diff --git a/src/builder.rs b/src/builder.rs index e07e31f1..9b028882 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,6 +2,7 @@ use core::fmt; use core::iter; +use std::collections::HashMap; use ff::Field; use nonempty::NonEmpty; @@ -57,21 +58,23 @@ impl From for Error { } /// Information about a specific note to be spent in an [`Action`]. -#[derive(Debug)] +#[derive(Debug, Clone)] struct SpendInfo { dummy_sk: Option, fvk: FullViewingKey, scope: Scope, note: Note, merkle_path: MerklePath, + // a flag to indicate whether the value of the note will be counted in the value sum of the action. + split_flag: bool, } impl SpendInfo { /// Defined in [Zcash Protocol Spec ยง 4.8.3: Dummy Notes (Orchard)][orcharddummynotes]. /// /// [orcharddummynotes]: https://zips.z.cash/protocol/nu5.pdf#orcharddummynotes - fn dummy(rng: &mut impl RngCore) -> Self { - let (sk, fvk, note) = Note::dummy(rng, None); + fn dummy(note_type: NoteType, rng: &mut impl RngCore) -> Self { + let (sk, fvk, note) = Note::dummy(rng, None, note_type); let merkle_path = MerklePath::dummy(rng); SpendInfo { @@ -82,16 +85,25 @@ impl SpendInfo { scope: Scope::External, note, merkle_path, + split_flag: false, } } + + /// Duplicates the spend info and set the split flag to `true`. + fn create_split_spend(&self) -> Self { + let mut split_spend = self.clone(); + split_spend.split_flag = true; + split_spend + } } /// Information about a specific recipient to receive funds in an [`Action`]. -#[derive(Debug)] +#[derive(Debug, Clone)] struct RecipientInfo { ovk: Option, recipient: Address, value: NoteValue, + note_type: NoteType, memo: Option<[u8; 512]>, } @@ -107,6 +119,7 @@ impl RecipientInfo { ovk: None, recipient, value: NoteValue::zero(), + note_type: NoteType::native(), memo: None, } } @@ -130,7 +143,17 @@ impl ActionInfo { } /// Returns the value sum for this action. + /// Split notes does not contribute to the value sum. fn value_sum(&self) -> ValueSum { + // TODO: Aurel, uncomment when circuit for split flag is implemented. + // let spent_value = self + // .spend + // .split_flag + // .then(|| self.spend.note.value()) + // .unwrap_or_else(NoteValue::zero); + // + // spent_value - self.output.value + self.spend.note.value() - self.output.value } @@ -140,8 +163,15 @@ impl ActionInfo { /// /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend fn build(self, mut rng: impl RngCore) -> (Action, Circuit) { + assert_eq!( + self.output.note_type, + self.spend.note.note_type(), + "spend and recipient note types must be equal" + ); + let v_net = self.value_sum(); - let cv_net = ValueCommitment::derive(v_net, self.rcv, NoteType::native()); + let note_type = self.output.note_type; + let cv_net = ValueCommitment::derive(v_net, self.rcv, note_type); let nf_old = self.spend.note.nullifier(&self.spend.fvk); let sender_address = self.spend.note.recipient(); @@ -151,7 +181,6 @@ impl ActionInfo { let ak: SpendValidatingKey = self.spend.fvk.clone().into(); let alpha = pallas::Scalar::random(&mut rng); let rk = ak.randomize(&alpha); - let note_type = self.spend.note.note_type(); let note = Note::new( self.output.recipient, @@ -201,6 +230,7 @@ impl ActionInfo { g_d_old: Some(sender_address.g_d()), pk_d_old: Some(*sender_address.pk_d()), v_old: Some(self.spend.note.value()), + //split: Some(self.spend.split_flag), rho_old: Some(rho_old), psi_old: Some(psi_old), rcm_old: Some(rcm_old), @@ -274,6 +304,7 @@ impl Builder { scope, note, merkle_path, + split_flag: false, }); Ok(()) @@ -285,6 +316,7 @@ impl Builder { ovk: Option, recipient: Address, value: NoteValue, + note_type: NoteType, memo: Option<[u8; 512]>, ) -> Result<(), &'static str> { if !self.flags.outputs_enabled() { @@ -295,6 +327,7 @@ impl Builder { ovk, recipient, value, + note_type, memo, }); @@ -306,23 +339,31 @@ impl Builder { /// The returned bundle will have no proof or signatures; these can be applied with /// [`Bundle::create_proof`] and [`Bundle::apply_signatures`] respectively. pub fn build>( - mut self, + self, mut rng: impl RngCore, ) -> Result, V>, Error> { + let mut pre_actions: Vec<_> = Vec::new(); + // Pair up the spends and recipients, extending with dummy values as necessary. - let pre_actions: Vec<_> = { - let num_spends = self.spends.len(); - let num_recipients = self.recipients.len(); + for (note_type, (mut spends, mut recipients)) in partition(&self.spends, &self.recipients) { + let num_spends = spends.len(); + let num_recipients = recipients.len(); let num_actions = [num_spends, num_recipients, MIN_ACTIONS] .iter() .max() .cloned() .unwrap(); - self.spends.extend( - iter::repeat_with(|| SpendInfo::dummy(&mut rng)).take(num_actions - num_spends), + // use the first spend to create split spend(s) or create a dummy if empty. + let dummy_spend = spends.first().map_or_else( + || SpendInfo::dummy(note_type, &mut rng), + |s| s.create_split_spend(), ); - self.recipients.extend( + + // Extend the spends and recipients with dummy values. + spends.extend(iter::repeat_with(|| dummy_spend.clone()).take(num_actions - num_spends)); + + recipients.extend( iter::repeat_with(|| RecipientInfo::dummy(&mut rng)) .take(num_actions - num_recipients), ); @@ -330,15 +371,16 @@ impl Builder { // Shuffle the spends and recipients, 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. - self.spends.shuffle(&mut rng); - self.recipients.shuffle(&mut rng); + spends.shuffle(&mut rng); + recipients.shuffle(&mut rng); - self.spends - .into_iter() - .zip(self.recipients.into_iter()) - .map(|(spend, recipient)| ActionInfo::new(spend, recipient, &mut rng)) - .collect() - }; + pre_actions.extend( + spends + .into_iter() + .zip(recipients.into_iter()) + .map(|(spend, recipient)| ActionInfo::new(spend, recipient, &mut rng)), + ); + } // Move some things out of self that we will need. let flags = self.flags; @@ -390,6 +432,30 @@ impl Builder { } } +/// partition a list of spends and recipients by note types. +fn partition( + spends: &[SpendInfo], + recipients: &[RecipientInfo], +) -> HashMap, Vec)> { + let mut hm = HashMap::new(); + + for s in spends.iter() { + hm.entry(s.note.note_type()) + .or_insert((vec![], vec![])) + .0 + .push(s.clone()); + } + + for r in recipients.iter() { + hm.entry(r.note_type) + .or_insert((vec![], vec![])) + .1 + .push(r.clone()) + } + + hm +} + /// Marker trait representing bundle signatures in the process of being created. pub trait InProgressSignatures: fmt::Debug { /// The authorization type of an Orchard action in the process of being authorized. @@ -654,6 +720,7 @@ pub mod testing { use proptest::collection::vec; use proptest::prelude::*; + use crate::note::NoteType; use crate::{ address::testing::arb_address, bundle::{Authorized, Bundle, Flags}, @@ -681,7 +748,7 @@ pub mod testing { sk: SpendingKey, anchor: Anchor, notes: Vec<(Note, MerklePath)>, - recipient_amounts: Vec<(Address, NoteValue)>, + recipient_amounts: Vec<(Address, NoteValue, NoteType)>, } impl ArbitraryBundleInputs { @@ -695,12 +762,12 @@ pub mod testing { builder.add_spend(fvk.clone(), note, path).unwrap(); } - for (addr, value) in self.recipient_amounts.into_iter() { + for (addr, value, note_type) in self.recipient_amounts.into_iter() { let scope = fvk.scope_for_address(&addr).unwrap(); let ovk = fvk.to_ovk(scope); builder - .add_recipient(Some(ovk.clone()), addr, value, None) + .add_recipient(Some(ovk.clone()), addr, value, note_type, None) .unwrap(); } @@ -734,9 +801,12 @@ pub mod testing { recipient_amounts in vec( arb_address().prop_flat_map(move |a| { arb_positive_note_value(MAX_NOTE_VALUE / n_recipients as u64) - .prop_map(move |v| (a, v)) + .prop_map(move |v| { + // replace note type with arb_note_type + (a,v, NoteType::native()) + }) }), - n_recipients as usize + n_recipients as usize, ), rng_seed in prop::array::uniform32(prop::num::u8::ANY) ) -> ArbitraryBundleInputs { @@ -784,6 +854,8 @@ mod tests { use rand::rngs::OsRng; use super::Builder; + // use crate::keys::{IssuerAuthorizingKey, IssuerValidatingKey}; + use crate::note::NoteType; use crate::{ bundle::{Authorized, Bundle, Flags}, circuit::ProvingKey, @@ -808,8 +880,38 @@ mod tests { ); builder - .add_recipient(None, recipient, NoteValue::from_raw(5000), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(5000), + NoteType::native(), + None, + ) .unwrap(); + // + // builder + // .add_recipient( + // None, + // recipient, + // NoteValue::from_raw(3), + // NoteType::native(), + // None, + // ) + // .unwrap(); + // + // + // let isk = IssuerAuthorizingKey::from(&sk); + // + // builder + // .add_recipient( + // None, + // recipient, + // NoteValue::from_raw(5), + // NoteType::derive(&IssuerValidatingKey::from(&isk), Vec::new()), + // None, + // ) + // .unwrap(); + let bundle: Bundle = builder .build(&mut rng) .unwrap() diff --git a/src/circuit.rs b/src/circuit.rs index 9e5634e7..e8e6efe9 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -104,6 +104,7 @@ pub struct Circuit { pub(crate) g_d_old: Option, pub(crate) pk_d_old: Option, pub(crate) v_old: Option, + // pub(crate) split: Option, pub(crate) rho_old: Option, pub(crate) psi_old: Option, pub(crate) rcm_old: Option, @@ -891,7 +892,7 @@ mod tests { }; fn generate_circuit_instance(mut rng: R) -> (Circuit, Instance) { - let (_, fvk, spent_note) = Note::dummy(&mut rng, None); + let (_, fvk, spent_note) = Note::dummy(&mut rng, None, NoteType::native()); let sender_address = spent_note.recipient(); let nk = *fvk.nk(); @@ -901,7 +902,7 @@ mod tests { let alpha = pallas::Scalar::random(&mut rng); let rk = ak.randomize(&alpha); - let (_, _, output_note) = Note::dummy(&mut rng, Some(nf_old)); + let (_, _, output_note) = Note::dummy(&mut rng, Some(nf_old), NoteType::native()); let cmx = output_note.commitment().into(); let value = spent_note.value() - output_note.value(); @@ -918,6 +919,7 @@ mod tests { g_d_old: Some(sender_address.g_d()), pk_d_old: Some(*sender_address.pk_d()), v_old: Some(spent_note.value()), + // split: Some(false), rho_old: Some(spent_note.rho()), psi_old: Some(spent_note.rseed().psi(&spent_note.rho())), rcm_old: Some(spent_note.rseed().rcm(&spent_note.rho())), @@ -1118,6 +1120,7 @@ mod tests { psi_new: None, rcm_new: None, rcv: None, + // split: None, }; halo2_proofs::dev::CircuitLayout::default() .show_labels(false) diff --git a/src/note.rs b/src/note.rs index 60f1154c..9e56e31d 100644 --- a/src/note.rs +++ b/src/note.rs @@ -163,6 +163,7 @@ impl Note { pub(crate) fn dummy( rng: &mut impl RngCore, rho: Option, + note_type: NoteType, ) -> (SpendingKey, FullViewingKey, Self) { let sk = SpendingKey::random(rng); let fvk: FullViewingKey = (&sk).into(); @@ -171,7 +172,7 @@ impl Note { let note = Note::new( recipient, NoteValue::zero(), - NoteType::native(), + note_type, rho.unwrap_or_else(|| Nullifier::dummy(rng)), rng, ); @@ -296,7 +297,7 @@ pub mod testing { } prop_compose! { - /// Generate an action without authorization data. + /// Generate an arbitrary note pub fn arb_note(value: NoteValue)( recipient in arb_address(), rho in arb_nullifier(), diff --git a/src/note/note_type.rs b/src/note/note_type.rs index 93a79af7..640f2dfa 100644 --- a/src/note/note_type.rs +++ b/src/note/note_type.rs @@ -1,13 +1,14 @@ use group::GroupEncoding; use halo2_proofs::arithmetic::CurveExt; use pasta_curves::pallas; +use std::hash::{Hash, Hasher}; use subtle::{Choice, ConstantTimeEq, CtOption}; use crate::constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_V_BYTES}; use crate::keys::IssuerValidatingKey; /// Note type identifier. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Eq)] pub struct NoteType(pallas::Point); pub const MAX_ASSET_DESCRIPTION_SIZE: usize = 512; @@ -62,6 +63,19 @@ impl NoteType { } } +impl Hash for NoteType { + fn hash(&self, h: &mut H) { + h.write(&self.to_bytes()); + h.finish(); + } +} + +impl PartialEq for NoteType { + fn eq(&self, other: &Self) -> bool { + bool::from(self.0.ct_eq(&other.0)) + } +} + /// Generators for property testing. #[cfg(any(test, feature = "test-dependencies"))] #[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))] @@ -83,4 +97,12 @@ pub mod testing { NoteType::derive(&IssuerValidatingKey::from(&isk), bytes64) } } + + prop_compose! { + /// Generate the native note type + pub fn native_note_type()(_i in 0..10) -> NoteType { + // TODO: remove _i + NoteType::native() + } + } } diff --git a/src/tree.rs b/src/tree.rs index c2854200..7e960131 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -77,7 +77,7 @@ impl Anchor { /// The Merkle path from a leaf of the note commitment tree /// to its anchor. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MerklePath { position: u32, auth_path: [MerkleHashOrchard; MERKLE_DEPTH_ORCHARD], diff --git a/src/value.rs b/src/value.rs index 8db1839a..bfbc7868 100644 --- a/src/value.rs +++ b/src/value.rs @@ -38,6 +38,7 @@ use core::fmt::{self, Debug}; use core::iter::Sum; use core::ops::{Add, RangeInclusive, Sub}; +use std::ops::Neg; use bitvec::{array::BitArray, order::Lsb0}; use ff::{Field, PrimeField}; @@ -187,6 +188,18 @@ impl Add for ValueSum { } } +impl Neg for ValueSum { + type Output = Option; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn neg(self) -> Self::Output { + self.0 + .checked_neg() + .filter(|v| VALUE_SUM_RANGE.contains(v)) + .map(ValueSum) + } +} + impl<'a> Sum<&'a ValueSum> for Result { fn sum>(iter: I) -> Self { iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + *v).ok_or(OverflowError)) @@ -407,7 +420,7 @@ pub mod testing { #[cfg(test)] mod tests { - use crate::note::note_type::testing::arb_note_type; + use crate::note::note_type::testing::{arb_note_type, native_note_type}; use crate::note::NoteType; use proptest::prelude::*; @@ -417,24 +430,42 @@ mod tests { }; use crate::primitives::redpallas; - fn _bsk_consistent_with_bvk(values: &[(ValueSum, ValueCommitTrapdoor)], note_type: NoteType) { - let value_balance = values + fn _bsk_consistent_with_bvk( + native_values: &[(ValueSum, ValueCommitTrapdoor, NoteType)], + arb_values: &[(ValueSum, ValueCommitTrapdoor, NoteType)], + neg_trapdoors: &[ValueCommitTrapdoor], + ) { + // for each arb value, create a negative value with a different trapdoor + let neg_arb_values: Vec<_> = arb_values .iter() - .map(|(value, _)| value) + .cloned() + .zip(neg_trapdoors.iter().cloned()) + .map(|((value, _, note_type), rcv)| ((-value).unwrap(), rcv, note_type)) + .collect(); + + let native_value_balance = native_values + .iter() + .map(|(value, _, _)| value) .sum::>() .expect("we generate values that won't overflow"); + let values = [native_values, arb_values, &neg_arb_values].concat(); + let bsk = values .iter() - .map(|(_, rcv)| rcv) + .map(|(_, rcv, _)| rcv) .sum::() .into_bsk(); let bvk = (values .iter() - .map(|(value, rcv)| ValueCommitment::derive(*value, *rcv, note_type)) + .map(|(value, rcv, note_type)| ValueCommitment::derive(*value, *rcv, *note_type)) .sum::() - - ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero(), note_type)) + - ValueCommitment::derive( + native_value_balance, + ValueCommitTrapdoor::zero(), + NoteType::native(), + )) .into_bvk(); assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); @@ -442,18 +473,34 @@ mod tests { proptest! { #[test] - fn bsk_consistent_with_bvk( - values in (1usize..10).prop_flat_map(|n_values| + fn bsk_consistent_with_bvk_native_only( + native_values in (1usize..10).prop_flat_map(|n_values| arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound| - prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor()), n_values) + prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor(), native_note_type()), n_values) ) ), - arb_note_type in arb_note_type(), ) { - // Test with native note type (zec) - _bsk_consistent_with_bvk(&values, NoteType::native()); - // Test with arbitrary note type - _bsk_consistent_with_bvk(&values, arb_note_type); + // Test with native note type (zec) only + _bsk_consistent_with_bvk(&native_values, &[], &[]); + } + } + + proptest! { + #[test] + fn bsk_consistent_with_bvk( + native_values in (1usize..10).prop_flat_map(|n_values| + arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound| + prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor(), native_note_type()), n_values) + ) + ), + (arb_values,neg_trapdoors) in (1usize..10).prop_flat_map(|n_values| + (arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound| + prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor(), arb_note_type()), n_values) + ), prop::collection::vec(arb_trapdoor(), n_values)) + ), + ) { + // Test with native and arbitrary note types + _bsk_consistent_with_bvk(&native_values, &arb_values, &neg_trapdoors); } } } diff --git a/tests/builder.rs b/tests/builder.rs index 35c53976..4329efc7 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -1,4 +1,5 @@ use incrementalmerkletree::{bridgetree::BridgeTree, Hashable, Tree}; +use orchard::note::NoteType; use orchard::{ builder::Builder, bundle::{Authorized, Flags}, @@ -43,7 +44,13 @@ fn bundle_chain() { let mut builder = Builder::new(Flags::from_parts(false, true), anchor); assert_eq!( - builder.add_recipient(None, recipient, NoteValue::from_raw(5000), None), + builder.add_recipient( + None, + recipient, + NoteValue::from_raw(5000), + NoteType::native(), + None + ), Ok(()) ); let unauthorized = builder.build(&mut rng).unwrap(); @@ -85,7 +92,13 @@ fn bundle_chain() { let mut builder = Builder::new(Flags::from_parts(true, true), anchor); assert_eq!(builder.add_spend(fvk, note, merkle_path), Ok(())); assert_eq!( - builder.add_recipient(None, recipient, NoteValue::from_raw(5000), None), + builder.add_recipient( + None, + recipient, + NoteValue::from_raw(5000), + NoteType::native(), + None + ), Ok(()) ); let unauthorized = builder.build(&mut rng).unwrap();