diff --git a/.circleci/config.yml b/.circleci/config.yml index ba37a7890..9dead1280 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor docker: - - image: cimg/rust:1.59.0 + - image: cimg/rust:1.61.0 # Add steps to the job # See: https://circleci.com/docs/2.0/configuration-reference/#steps steps: diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 21f631759..ba1ceea59 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 override: true - name: Run benchmark run: cargo bench -- --output-format bencher | tee output.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 101cee1ec..66b8c4d3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 override: true - name: Run tests uses: actions-rs/cargo@v1 @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 override: true # Build benchmarks to prevent bitrot - name: Build benchmarks @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 override: true - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 @@ -90,7 +90,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 override: true - name: cargo fetch uses: actions-rs/cargo@v1 @@ -113,7 +113,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 diff --git a/.github/workflows/lints-stable.yml b/.github/workflows/lints-stable.yml index 88a727090..a000ab449 100644 --- a/.github/workflows/lints-stable.yml +++ b/.github/workflows/lints-stable.yml @@ -5,19 +5,19 @@ on: pull_request jobs: clippy: - name: Clippy (1.61) + name: Clippy (1.61.0) timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.61 + toolchain: 1.61.0 components: clippy override: true - name: Run Clippy uses: actions-rs/clippy-check@v1 with: - name: Clippy (1.61) + name: Clippy (1.61.0) token: ${{ secrets.GITHUB_TOKEN }} args: --all-features --all-targets -- -D warnings diff --git a/.gitignore b/.gitignore index d7f36bd31..f4ca20140 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ Cargo.lock .vscode .idea action-circuit-layout.png -proptest-regressions/*.txt diff --git a/Cargo.toml b/Cargo.toml index ed956ef27..cb7508702 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ authors = [ "Kris Nuttycombe ", ] edition = "2021" -rust-version = "1.61" +rust-version = "1.61.0" description = "The Orchard shielded transaction protocol" license-file = "LICENSE-BOSL" repository = "https://github.com/zcash/orchard" diff --git a/rust-toolchain b/rust-toolchain index 4213d88dc..91951fd8a 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.61 +1.61.0 diff --git a/src/builder.rs b/src/builder.rs index 5c6147022..4229cbb28 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -251,6 +251,7 @@ impl ActionInfo { pub struct Builder { spends: Vec, recipients: Vec, + burn: HashMap, flags: Flags, anchor: Anchor, } @@ -261,6 +262,7 @@ impl Builder { Builder { spends: vec![], recipients: vec![], + burn: HashMap::new(), flags, anchor, } @@ -337,6 +339,17 @@ impl Builder { Ok(()) } + /// Add an instruction to burn a given amount of a specific asset. + pub fn add_burn(&mut self, asset: AssetId, value: NoteValue) -> Result<(), &'static str> { + if asset.is_native().into() { + return Err("Burning is only possible for non-native assets"); + } + let cur = *self.burn.get(&asset).unwrap_or(&ValueSum::zero()); + let sum = (cur + value).ok_or("Orchard ValueSum operation overflowed")?; + self.burn.insert(asset, sum); + Ok(()) + } + /// The net value of the bundle to be built. The value of all spends, /// minus the value of all outputs. /// @@ -366,7 +379,7 @@ 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>( + pub fn build + Copy + Into>( self, mut rng: impl RngCore, ) -> Result, V>, Error> { @@ -419,16 +432,14 @@ impl Builder { let anchor = self.anchor; // Determine the value balance for this bundle, ensuring it is valid. - let value_balance = pre_actions + let native_value_balance: V = pre_actions .iter() + .filter(|action| action.spend.note.asset().is_native().into()) .fold(Some(ValueSum::zero()), |acc, action| { acc? + action.value_sum() }) - .ok_or(OverflowError)?; - - let result_value_balance: V = i64::try_from(value_balance) - .map_err(Error::ValueSum) - .and_then(|i| V::try_from(i).map_err(|_| Error::ValueSum(value::OverflowError)))?; + .ok_or(OverflowError)? + .into()?; // Compute the transaction binding signing key. let bsk = pre_actions @@ -441,26 +452,26 @@ impl Builder { let (actions, circuits): (Vec<_>, Vec<_>) = pre_actions.into_iter().map(|a| a.build(&mut rng)).unzip(); - // Verify that bsk and bvk are consistent. - let bvk = (actions.iter().map(|a| a.cv_net()).sum::() - - ValueCommitment::derive( - value_balance, - ValueCommitTrapdoor::zero(), - AssetId::native(), - )) - .into_bvk(); - assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); - - Ok(Bundle::from_parts( + let bundle = Bundle::from_parts( NonEmpty::from_vec(actions).unwrap(), flags, - result_value_balance, + native_value_balance, + self.burn + .into_iter() + .map(|(asset, value)| Ok((asset, value.into()?))) + .collect::>()?, anchor, InProgress { proof: Unproven { circuits }, sigs: Unauthorized { bsk }, }, - )) + ); + + assert_eq!( + redpallas::VerificationKey::from(&bundle.authorization().sigs.bsk), + bundle.binding_validating_key() + ); + Ok(bundle) } } @@ -785,7 +796,7 @@ pub mod testing { impl ArbitraryBundleInputs { /// Create a bundle from the set of arbitrary bundle inputs. - fn into_bundle>(mut self) -> Bundle { + fn into_bundle + Copy + Into>(mut self) -> Bundle { let fvk = FullViewingKey::from(&self.sk); let flags = Flags::from_parts(true, true); let mut builder = Builder::new(flags, self.anchor); @@ -866,14 +877,15 @@ pub mod testing { } /// Produce an arbitrary valid Orchard bundle using a random spending key. - pub fn arb_bundle + Debug>() -> impl Strategy> { + pub fn arb_bundle + Debug + Copy + Into>( + ) -> impl Strategy> { arb_spending_key() .prop_flat_map(arb_bundle_inputs) .prop_map(|inputs| inputs.into_bundle::()) } /// Produce an arbitrary valid Orchard bundle using a specified spending key. - pub fn arb_bundle_with_key + Debug>( + pub fn arb_bundle_with_key + Debug + Copy + Into>( k: SpendingKey, ) -> impl Strategy> { arb_bundle_inputs(k).prop_map(|inputs| inputs.into_bundle::()) diff --git a/src/bundle.rs b/src/bundle.rs index 6f7e0bce9..cbc66e796 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -140,6 +140,9 @@ pub struct Bundle { /// /// This is the sum of Orchard spends minus the sum of Orchard outputs. value_balance: V, + /// Assets intended for burning + /// TODO We need to add a consensus check to make sure that it is impossible to burn ZEC. + burn: Vec<(AssetId, V)>, /// The root of the Orchard commitment tree that this bundle commits to. anchor: Anchor, /// The authorization for this bundle. @@ -172,6 +175,7 @@ impl Bundle { actions: NonEmpty>, flags: Flags, value_balance: V, + burn: Vec<(AssetId, V)>, anchor: Anchor, authorization: T, ) -> Self { @@ -179,6 +183,7 @@ impl Bundle { actions, flags, value_balance, + burn, anchor, authorization, } @@ -214,8 +219,8 @@ impl Bundle { } /// Construct a new bundle by applying a transformation that might fail - /// to the value balance. - pub fn try_map_value_balance Result>( + /// to the value balance and balances of assets to burn. + pub fn try_map_value_balance Result>( self, f: F, ) -> Result, E> { @@ -223,6 +228,11 @@ impl Bundle { actions: self.actions, flags: self.flags, value_balance: f(self.value_balance)?, + burn: self + .burn + .into_iter() + .map(|(asset, value)| Ok((asset, f(value)?))) + .collect::, E>>()?, anchor: self.anchor, authorization: self.authorization, }) @@ -244,6 +254,7 @@ impl Bundle { value_balance: self.value_balance, anchor: self.anchor, authorization: step(context, authorization), + burn: self.burn, } } @@ -267,6 +278,7 @@ impl Bundle { value_balance: self.value_balance, anchor: self.anchor, authorization: step(context, authorization)?, + burn: self.burn, }) } @@ -386,7 +398,19 @@ impl> Bundle { ValueSum::from_raw(self.value_balance.into()), ValueCommitTrapdoor::zero(), AssetId::native(), - )) + ) + - self + .burn + .clone() + .into_iter() + .map(|(asset, value)| { + ValueCommitment::derive( + ValueSum::from_raw(value.into()), + ValueCommitTrapdoor::zero(), + asset, + ) + }) + .sum::()) .into_bvk() } } @@ -503,6 +527,9 @@ pub mod testing { use super::{Action, Authorization, Authorized, Bundle, Flags}; pub use crate::action::testing::{arb_action, arb_unauthorized_action}; + use crate::note::asset_id::testing::zsa_asset_id; + use crate::note::AssetId; + use crate::value::testing::arb_value_sum; /// Marker for an unauthorized bundle with no proofs or signatures. #[derive(Debug)] @@ -562,6 +589,13 @@ pub mod testing { }) } + prop_compose! { + /// Create an arbitrary vector of assets to burn. + pub fn arb_asset_to_burn()(asset_id in zsa_asset_id(), value in arb_value_sum()) -> (AssetId, ValueSum) { + (asset_id, value) + } + } + prop_compose! { /// Create an arbitrary set of flags. pub fn arb_flags()(spends_enabled in prop::bool::ANY, outputs_enabled in prop::bool::ANY) -> Flags { @@ -589,7 +623,8 @@ pub mod testing { ( acts in vec(arb_unauthorized_action_n(n_actions, flags), n_actions), anchor in arb_base().prop_map(Anchor::from), - flags in Just(flags) + flags in Just(flags), + burn in vec(arb_asset_to_burn(), 1usize..10) ) -> Bundle { let (balances, actions): (Vec, Vec>) = acts.into_iter().unzip(); @@ -597,8 +632,9 @@ pub mod testing { NonEmpty::from_vec(actions).unwrap(), flags, balances.into_iter().sum::>().unwrap(), + burn, anchor, - Unauthorized + Unauthorized, ) } } @@ -618,7 +654,8 @@ pub mod testing { rng_seed in prop::array::uniform32(prop::num::u8::ANY), fake_proof in vec(prop::num::u8::ANY, 1973), fake_sighash in prop::array::uniform32(prop::num::u8::ANY), - flags in Just(flags) + flags in Just(flags), + burn in vec(arb_asset_to_burn(), 1usize..10) ) -> Bundle { let (balances, actions): (Vec, Vec>) = acts.into_iter().unzip(); let rng = StdRng::from_seed(rng_seed); @@ -627,11 +664,12 @@ pub mod testing { NonEmpty::from_vec(actions).unwrap(), flags, balances.into_iter().sum::>().unwrap(), + burn, anchor, Authorized { proof: Proof::new(fake_proof), binding_signature: sk.sign(rng, &fake_sighash), - } + }, ) } } diff --git a/src/value.rs b/src/value.rs index 02d845b4c..dd3a5546d 100644 --- a/src/value.rs +++ b/src/value.rs @@ -53,6 +53,7 @@ use pasta_curves::{ use rand::RngCore; use subtle::CtOption; +use crate::builder::Error; use crate::note::AssetId; use crate::{ constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_R_BYTES}, @@ -129,6 +130,12 @@ impl From<&NoteValue> for Assigned { } } +impl From for i128 { + fn from(value: NoteValue) -> Self { + value.0 as i128 + } +} + impl Sub for NoteValue { type Output = ValueSum; @@ -181,15 +188,21 @@ impl ValueSum { sign, ) } + + pub(crate) fn into>(self) -> Result { + i64::try_from(self) + .map_err(Error::ValueSum) + .and_then(|i| V::try_from(i).map_err(|_| Error::ValueSum(OverflowError))) + } } -impl Add for ValueSum { +impl> Add for ValueSum { type Output = Option; #[allow(clippy::suspicious_arithmetic_impl)] - fn add(self, rhs: Self) -> Self::Output { + fn add(self, rhs: T) -> Self::Output { self.0 - .checked_add(rhs.0) + .checked_add(rhs.into()) .filter(|v| VALUE_SUM_RANGE.contains(v)) .map(ValueSum) } @@ -227,6 +240,12 @@ impl TryFrom for i64 { } } +impl From for i128 { + fn from(value: ValueSum) -> Self { + value.0 + } +} + /// The blinding factor for a [`ValueCommitment`]. #[derive(Clone, Copy, Debug)] pub struct ValueCommitTrapdoor(pallas::Scalar); @@ -452,10 +471,11 @@ mod tests { }; use crate::primitives::redpallas; - fn _bsk_consistent_with_bvk( + fn check_binding_signature( native_values: &[(ValueSum, ValueCommitTrapdoor, AssetId)], arb_values: &[(ValueSum, ValueCommitTrapdoor, AssetId)], neg_trapdoors: &[ValueCommitTrapdoor], + arb_values_to_burn: &[(ValueSum, ValueCommitTrapdoor, AssetId)], ) { // for each arb value, create a negative value with a different trapdoor let neg_arb_values: Vec<_> = arb_values @@ -471,7 +491,13 @@ mod tests { .sum::>() .expect("we generate values that won't overflow"); - let values = [native_values, arb_values, &neg_arb_values].concat(); + let values = [ + native_values, + arb_values, + &neg_arb_values, + arb_values_to_burn, + ] + .concat(); let bsk = values .iter() @@ -480,14 +506,20 @@ mod tests { .into_bsk(); let bvk = (values - .iter() - .map(|(value, rcv, asset)| ValueCommitment::derive(*value, *rcv, *asset)) + .into_iter() + .map(|(value, rcv, asset)| ValueCommitment::derive(value, rcv, asset)) .sum::() - ValueCommitment::derive( native_value_balance, ValueCommitTrapdoor::zero(), AssetId::native(), - )) + ) + - arb_values_to_burn + .iter() + .map(|(value, _, asset)| { + ValueCommitment::derive(*value, ValueCommitTrapdoor::zero(), *asset) + }) + .sum::()) .into_bvk(); assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); @@ -495,34 +527,26 @@ mod tests { proptest! { #[test] - 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(), native_asset_id()), n_values) - ) - ), - ) { - // Test with native note type (zec) only - _bsk_consistent_with_bvk(&native_values, &[], &[]); - } - } - - proptest! { - #[test] - fn bsk_consistent_with_bvk( + fn bsk_consistent_with_bvk_native_with_zsa_transfer_and_burning( 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_asset_id()), n_values) ) ), - (arb_values,neg_trapdoors) in (1usize..10).prop_flat_map(|n_values| + (asset_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_asset_id()), n_values) ), prop::collection::vec(arb_trapdoor(), n_values)) ), + burn_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(), arb_asset_id()), n_values)) + ) ) { - // Test with native note type (zec) - _bsk_consistent_with_bvk(&native_values, &arb_values, &neg_trapdoors); + check_binding_signature(&native_values, &[], &[], &[]); + check_binding_signature(&native_values, &[], &[], &burn_values); + check_binding_signature(&native_values, &asset_values, &neg_trapdoors, &[]); + check_binding_signature(&native_values, &asset_values, &neg_trapdoors, &burn_values); } } } diff --git a/tests/zsa.rs b/tests/zsa.rs index f65b18616..9aad802d5 100644 --- a/tests/zsa.rs +++ b/tests/zsa.rs @@ -231,32 +231,31 @@ struct TestOutputInfo { fn build_and_verify_bundle( spends: Vec<&TestSpendInfo>, outputs: Vec, + assets_to_burn: Vec<(AssetId, NoteValue)>, anchor: Anchor, expected_num_actions: usize, keys: &Keychain, -) { +) -> Result<(), &'static str> { let rng = OsRng; let shielded_bundle: Bundle<_, i64> = { let mut builder = Builder::new(Flags::from_parts(true, true), anchor); - spends.iter().for_each(|spend| { - assert_eq!( - builder.add_spend(keys.fvk().clone(), spend.note, spend.merkle_path().clone()), - Ok(()) - ); - }); - outputs.iter().for_each(|output| { - assert_eq!( - builder.add_recipient(None, keys.recipient, output.value, output.asset, None), - Ok(()) - ) - }); + spends.iter().try_for_each(|spend| { + builder.add_spend(keys.fvk().clone(), spend.note, spend.merkle_path().clone()) + })?; + outputs.iter().try_for_each(|output| { + builder.add_recipient(None, keys.recipient, output.value, output.asset, None) + })?; + assets_to_burn + .into_iter() + .try_for_each(|(asset, value)| builder.add_burn(asset, value))?; build_and_sign_bundle(builder, rng, keys.pk(), keys.sk()) }; // Verify the shielded bundle, currently without the proof. verify_bundle(&shielded_bundle, &keys.vk, false); assert_eq!(shielded_bundle.actions().len(), expected_num_actions); + Ok(()) } /// Issue several ZSA and native notes and spend them in different combinations, e.g. split and join @@ -268,20 +267,32 @@ fn zsa_issue_and_transfer() { let asset_descr = "zsa_asset"; // Prepare ZSA - let (zsa_note1, zsa_note2) = issue_zsa_notes(asset_descr, &keys); + let (zsa_note_1, zsa_note_2) = issue_zsa_notes(asset_descr, &keys); let (merkle_path1, merkle_path2, anchor) = - build_merkle_path_with_two_leaves(&zsa_note1, &zsa_note2); + build_merkle_path_with_two_leaves(&zsa_note_1, &zsa_note_2); let zsa_spend_1 = TestSpendInfo { - note: zsa_note1, + note: zsa_note_1, merkle_path: merkle_path1, }; let zsa_spend_2 = TestSpendInfo { - note: zsa_note2, + note: zsa_note_2, merkle_path: merkle_path2, }; + let native_note = create_native_note(&keys); + let (native_merkle_path_1, native_merkle_path_2, native_anchor) = + build_merkle_path_with_two_leaves(&native_note, &zsa_note_1); + let native_spend: TestSpendInfo = TestSpendInfo { + note: native_note, + merkle_path: native_merkle_path_1, + }; + let zsa_spend_with_native: TestSpendInfo = TestSpendInfo { + note: zsa_note_1, + merkle_path: native_merkle_path_2, + }; + // --------------------------- Tests ----------------------------------------- // 1. Spend single ZSA note @@ -291,10 +302,12 @@ fn zsa_issue_and_transfer() { value: zsa_spend_1.note.value(), asset: zsa_spend_1.note.asset(), }], + vec![], anchor, 2, &keys, - ); + ) + .unwrap(); // 2. Split single ZSA note into 2 notes let delta = 2; // arbitrary number for value manipulation @@ -310,10 +323,12 @@ fn zsa_issue_and_transfer() { asset: zsa_spend_1.note.asset(), }, ], + vec![], anchor, 2, &keys, - ); + ) + .unwrap(); // 3. Join 2 ZSA notes into a single note build_and_verify_bundle( @@ -324,10 +339,12 @@ fn zsa_issue_and_transfer() { ), asset: zsa_spend_1.note.asset(), }], + vec![], anchor, 2, &keys, - ); + ) + .unwrap(); // 4. Take 2 ZSA notes and send them as 2 notes with different denomination build_and_verify_bundle( @@ -342,10 +359,12 @@ fn zsa_issue_and_transfer() { asset: zsa_spend_2.note.asset(), }, ], + vec![], anchor, 2, &keys, - ); + ) + .unwrap(); // 5. Spend single ZSA note, mixed with native note (shielding) build_and_verify_bundle( @@ -360,24 +379,14 @@ fn zsa_issue_and_transfer() { asset: AssetId::native(), }, ], + vec![], anchor, 4, &keys, - ); + ) + .unwrap(); // 6. Spend single ZSA note, mixed with native note (shielded to shielded) - let native_note = create_native_note(&keys); - let (native_merkle_path1, native_merkle_path2, native_anchor) = - build_merkle_path_with_two_leaves(&native_note, &zsa_note1); - let native_spend: TestSpendInfo = TestSpendInfo { - note: native_note, - merkle_path: native_merkle_path1, - }; - let zsa_spend_with_native: TestSpendInfo = TestSpendInfo { - note: zsa_note1, - merkle_path: native_merkle_path2, - }; - build_and_verify_bundle( vec![&zsa_spend_with_native, &native_spend], vec![ @@ -390,21 +399,23 @@ fn zsa_issue_and_transfer() { asset: AssetId::native(), }, ], + vec![], native_anchor, 4, &keys, - ); + ) + .unwrap(); // 7. Spend ZSA notes of different asset types let (zsa_note_t7, _) = issue_zsa_notes("zsa_asset2", &keys); let (merkle_path_t7_1, merkle_path_t7_2, anchor_t7) = - build_merkle_path_with_two_leaves(&zsa_note_t7, &zsa_note2); + build_merkle_path_with_two_leaves(&zsa_note_t7, &zsa_note_2); let zsa_spend_t7_1: TestSpendInfo = TestSpendInfo { note: zsa_note_t7, merkle_path: merkle_path_t7_1, }; let zsa_spend_t7_2: TestSpendInfo = TestSpendInfo { - note: zsa_note2, + note: zsa_note_2, merkle_path: merkle_path_t7_2, }; @@ -420,10 +431,12 @@ fn zsa_issue_and_transfer() { asset: zsa_spend_t7_2.note.asset(), }, ], + vec![], anchor_t7, 4, &keys, - ); + ) + .unwrap(); // 8. Same but wrong denomination let result = std::panic::catch_unwind(|| { @@ -439,10 +452,54 @@ fn zsa_issue_and_transfer() { asset: zsa_spend_t7_2.note.asset(), }, ], + vec![], anchor_t7, 4, &keys, - ); + ) + .unwrap(); }); assert!(result.is_err()); + + // 9. Burn ZSA assets + build_and_verify_bundle( + vec![&zsa_spend_1], + vec![], + vec![(zsa_spend_1.note.asset(), zsa_spend_1.note.value())], + anchor, + 2, + &keys, + ) + .unwrap(); + + // 10. Burn a partial amount of the ZSA assets + let value_to_burn = 3; + let value_to_transfer = zsa_spend_1.note.value().inner() - value_to_burn; + + build_and_verify_bundle( + vec![&zsa_spend_1], + vec![TestOutputInfo { + value: NoteValue::from_raw(value_to_transfer), + asset: zsa_spend_1.note.asset(), + }], + vec![(zsa_spend_1.note.asset(), NoteValue::from_raw(value_to_burn))], + anchor, + 2, + &keys, + ) + .unwrap(); + + // 11. Try to burn native asset - should fail + let result = build_and_verify_bundle( + vec![&native_spend], + vec![], + vec![(AssetId::native(), native_spend.note.value())], + native_anchor, + 2, + &keys, + ); + match result { + Ok(_) => panic!("Test should fail"), + Err(error) => assert_eq!(error, "Burning is only possible for non-native assets"), + } }