From d495bd402229f65d6c96d8cd126b3b9d693b0599 Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Tue, 14 Feb 2023 13:48:19 +1300
Subject: [PATCH 01/11] working on summary report improvements splits output
 and totals, adds balance checks, support for SCIs, tests

---
 account-keys/Cargo.toml             |   4 +-
 account-keys/src/account_keys.rs    |   8 +-
 transaction/extra/tests/verifier.rs | 192 +++++++------
 transaction/summary/Cargo.toml      |   6 +
 transaction/summary/src/data.rs     |  13 +-
 transaction/summary/src/lib.rs      |   2 +-
 transaction/summary/src/report.rs   | 345 ++++++++++++++++++++---
 transaction/summary/src/verifier.rs | 406 ++++++++++++++++++++++++++--
 8 files changed, 797 insertions(+), 179 deletions(-)

diff --git a/account-keys/Cargo.toml b/account-keys/Cargo.toml
index 2693101494..01f3471012 100644
--- a/account-keys/Cargo.toml
+++ b/account-keys/Cargo.toml
@@ -10,7 +10,7 @@ rust-version = { workspace = true }
 [features]
 std = ["mc-util-repr-bytes/alloc"]
 prost = ["dep:prost", "mc-util-repr-bytes/prost", "mc-crypto-keys/prost"]
-serde = ["mc-crypto-keys/serde"]
+serde = ["dep:serde", "mc-crypto-keys/serde", "curve25519-dalek/serde"]
 default = ["std", "prost", "serde", "mc-util-serial", "mc-crypto-digestible/default", "mc-crypto-hashes/default", "mc-crypto-keys/default"]
 
 [dependencies]
@@ -22,7 +22,7 @@ hex_fmt = "0.3"
 hkdf = "0.12.3"
 prost = { version = "0.11", optional = true, default-features = false, features = ["prost-derive"] }
 rand_core = { version = "0.6", default-features = false }
-serde = { version = "1.0", default-features = false }
+serde = { version = "1.0", optional = true, default-features = false, features = [ "alloc", "derive" ] }
 subtle = { version = "2", default-features = false }
 zeroize = { version = "1", default-features = false }
 
diff --git a/account-keys/src/account_keys.rs b/account-keys/src/account_keys.rs
index c86cf7550b..521bc982c0 100644
--- a/account-keys/src/account_keys.rs
+++ b/account-keys/src/account_keys.rs
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2022 The MobileCoin Foundation
+// Copyright (c) 2018-2023 The MobileCoin Foundation
 
 //! MobileCoin account keys.
 //!
@@ -37,6 +37,7 @@ use mc_util_from_random::FromRandom;
 #[cfg(feature = "prost")]
 use prost::Message;
 use rand_core::{CryptoRng, RngCore};
+#[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
 use zeroize::Zeroize;
 
@@ -49,10 +50,9 @@ pub use mc_core::{
 };
 
 /// A MobileCoin user's public subaddress.
-#[derive(
-    Clone, Deserialize, Digestible, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Zeroize,
-)]
+#[derive(Clone, Digestible, Eq, Hash, Ord, PartialEq, PartialOrd, Zeroize)]
 #[cfg_attr(feature = "prost", derive(Message))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub struct PublicAddress {
     /// The user's public subaddress view key 'C'.
     #[cfg_attr(feature = "prost", prost(message, required, tag = "1"))]
diff --git a/transaction/extra/tests/verifier.rs b/transaction/extra/tests/verifier.rs
index 327ba6d5e7..439f6e382a 100644
--- a/transaction/extra/tests/verifier.rs
+++ b/transaction/extra/tests/verifier.rs
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2022 The MobileCoin Foundation
+// Copyright (c) 2018-2023 The MobileCoin Foundation
 
 //! Tests of the streaming verifier
 
@@ -204,6 +204,7 @@ fn test_max_size_tx_summary_verification() {
         &tx_summary,
         &tx_summary_unblinding_data,
         *sender.view_private_key(),
+        sender.change_subaddress(),
     )
     .unwrap();
     assert_eq!(
@@ -212,17 +213,16 @@ fn test_max_size_tx_summary_verification() {
     );
 
     let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
-    let balance_changes: Vec<_> = report.balance_changes.iter().collect();
     assert_eq!(
-        balance_changes,
-        vec![
-            (&(TransactionEntity::Ourself, TokenId::from(0)), &-16000),
-            (
-                &(TransactionEntity::Address(recipient_hash), TokenId::from(0)),
-                &160
-            )
-        ]
+        &report.outputs,
+        &[(
+            TransactionEntity::OtherAddress(recipient_hash),
+            TokenId::from(0),
+            160
+        )]
     );
+    assert_eq!(&report.totals, &[(TokenId::from(0), 16000),]);
+
     assert_eq!(report.network_fee, Amount::new(15840, TokenId::from(0)));
 }
 
@@ -239,6 +239,7 @@ fn test_min_size_tx_summary_verification() {
         &tx_summary,
         &tx_summary_unblinding_data,
         *sender.view_private_key(),
+        sender.change_subaddress(),
     )
     .unwrap();
     assert_eq!(
@@ -247,17 +248,15 @@ fn test_min_size_tx_summary_verification() {
     );
 
     let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
-    let balance_changes: Vec<_> = report.balance_changes.iter().collect();
     assert_eq!(
-        balance_changes,
-        vec![
-            (&(TransactionEntity::Ourself, TokenId::from(0)), &-1000),
-            (
-                &(TransactionEntity::Address(recipient_hash), TokenId::from(0)),
-                &10
-            )
-        ]
+        &report.outputs,
+        &[(
+            TransactionEntity::OtherAddress(recipient_hash),
+            TokenId::from(0),
+            10
+        )]
     );
+    assert_eq!(&report.totals, &[(TokenId::from(0), 1000),]);
     assert_eq!(report.network_fee, Amount::new(990, TokenId::from(0)));
 }
 
@@ -333,6 +332,7 @@ fn test_two_input_tx_with_change_tx_summary_verification() {
             &tx_summary,
             &tx_summary_unblinding_data,
             *sender.view_private_key(),
+            sender.change_subaddress(),
         )
         .unwrap();
         assert_eq!(
@@ -341,23 +341,18 @@ fn test_two_input_tx_with_change_tx_summary_verification() {
         );
 
         let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
-        let balance_changes: Vec<_> = report
-            .balance_changes
-            .iter()
-            .map(|(x, y)| (x.clone(), *y))
-            .collect();
-        let expected = vec![
-            (
-                (TransactionEntity::Ourself, token_id),
-                -((value + value2 - change_value) as i64),
-            ),
-            (
-                (TransactionEntity::Address(recipient_hash), token_id),
-                (value + value2 - change_value - Mob::MINIMUM_FEE) as i64,
-            ),
-        ];
-
-        assert_eq!(balance_changes, expected);
+        assert_eq!(
+            &report.totals,
+            &[(token_id, (value + value2 - change_value) as i64),]
+        );
+        assert_eq!(
+            &report.outputs,
+            &[(
+                TransactionEntity::OtherAddress(recipient_hash),
+                token_id,
+                (value + value2 - change_value - Mob::MINIMUM_FEE)
+            ),]
+        );
         assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, token_id));
     }
 }
@@ -425,6 +420,7 @@ fn test_simple_tx_with_change_tx_summary_verification() {
             &tx_summary,
             &tx_summary_unblinding_data,
             *sender.view_private_key(),
+            sender.change_subaddress(),
         )
         .unwrap();
         assert_eq!(
@@ -433,23 +429,18 @@ fn test_simple_tx_with_change_tx_summary_verification() {
         );
 
         let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
-        let balance_changes: Vec<_> = report
-            .balance_changes
-            .iter()
-            .map(|(x, y)| (x.clone(), *y))
-            .collect();
-        let expected = vec![
-            (
-                (TransactionEntity::Ourself, token_id),
-                -((value - change_value) as i64),
-            ),
-            (
-                (TransactionEntity::Address(recipient_hash), token_id),
-                (value - change_value - Mob::MINIMUM_FEE) as i64,
-            ),
-        ];
-
-        assert_eq!(balance_changes, expected);
+        assert_eq!(
+            &report.totals,
+            &[(token_id, ((value - change_value) as i64)),]
+        );
+        assert_eq!(
+            &report.outputs,
+            &[(
+                TransactionEntity::OtherAddress(recipient_hash),
+                token_id,
+                (value - change_value - Mob::MINIMUM_FEE)
+            ),]
+        );
         assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, token_id));
     }
 }
@@ -520,6 +511,7 @@ fn test_two_output_tx_with_change_tx_summary_verification() {
             &tx_summary,
             &tx_summary_unblinding_data,
             *sender.view_private_key(),
+            sender.change_subaddress(),
         )
         .unwrap();
         assert_eq!(
@@ -529,28 +521,24 @@ fn test_two_output_tx_with_change_tx_summary_verification() {
 
         let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
         let recipient2_hash = ShortAddressHash::from(&recipient2.default_subaddress());
-        let balance_changes: Vec<_> = report
-            .balance_changes
-            .iter()
-            .map(|(x, y)| (x.clone(), *y))
-            .collect();
-        let mut expected = vec![
-            (
-                (TransactionEntity::Ourself, token_id),
-                -((value + value2 + Mob::MINIMUM_FEE) as i64),
-            ),
+        assert_eq!(
+            &report.totals,
+            &[(token_id, (value + value2 + Mob::MINIMUM_FEE) as i64),]
+        );
+        let mut outputs = vec![
             (
-                (TransactionEntity::Address(recipient_hash), token_id),
-                (value as i64),
+                TransactionEntity::OtherAddress(recipient_hash),
+                token_id,
+                value,
             ),
             (
-                (TransactionEntity::Address(recipient2_hash), token_id),
-                (value2 as i64),
+                TransactionEntity::OtherAddress(recipient2_hash),
+                token_id,
+                value2,
             ),
         ];
-        expected.sort();
-
-        assert_eq!(balance_changes, expected);
+        outputs.sort();
+        assert_eq!(&report.outputs[..], &outputs[..]);
         assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, token_id));
     }
 }
@@ -637,6 +625,7 @@ fn test_sci_tx_summary_verification() {
             &mut rng,
         )
         .unwrap();
+    let bob_hash = ShortAddressHash::from(&bob.default_subaddress());
 
     let unsigned_tx = builder
         .build_unsigned::<DefaultTxOutputsOrdering>()
@@ -650,6 +639,7 @@ fn test_sci_tx_summary_verification() {
         &tx_summary,
         &tx_summary_unblinding_data,
         *bob.view_private_key(),
+        bob.change_subaddress(),
     )
     .unwrap();
     assert_eq!(
@@ -657,23 +647,27 @@ fn test_sci_tx_summary_verification() {
         &signing_data.mlsag_signing_digest[..]
     );
 
-    let balance_changes: Vec<_> = report
-        .balance_changes
-        .iter()
-        .map(|(x, y)| (x.clone(), *y))
-        .collect();
-    let mut expected = vec![
+    // TODO: fix this test
+    assert_eq!(
+        &report.totals,
+        &[
+            // Bob spends 3x worth of token id 2 in the transaction
+            (token2, value2 as i64),
+        ]
+    );
+    let mut outputs = vec![
+        // Output to swap counterparty
+        (TransactionEntity::Swap, token2, value2),
+        // Converted output to ourself
         (
-            (TransactionEntity::Ourself, Mob::ID),
-            ((value - Mob::MINIMUM_FEE) as i64),
+            TransactionEntity::OurAddress(bob_hash),
+            Mob::ID,
+            value - Mob::MINIMUM_FEE,
         ),
-        ((TransactionEntity::Ourself, token2), -(value2 as i64)),
-        ((TransactionEntity::Swap, Mob::ID), -(value as i64)),
-        ((TransactionEntity::Swap, token2), (value2 as i64)),
     ];
-    expected.sort();
+    outputs.sort();
+    assert_eq!(&report.outputs[..], &outputs[..]);
 
-    assert_eq!(balance_changes, expected);
     assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, Mob::ID));
 }
 
@@ -773,6 +767,7 @@ fn test_sci_three_way_tx_summary_verification() {
         &tx_summary,
         &tx_summary_unblinding_data,
         *bob.view_private_key(),
+        bob.change_subaddress(),
     )
     .unwrap();
     assert_eq!(
@@ -780,24 +775,27 @@ fn test_sci_three_way_tx_summary_verification() {
         &signing_data.mlsag_signing_digest[..]
     );
 
-    let balance_changes: Vec<_> = report
-        .balance_changes
-        .iter()
-        .map(|(x, y)| (x.clone(), *y))
-        .collect();
-
     let charlie_hash = ShortAddressHash::from(&charlie.default_subaddress());
-    let mut expected = vec![
-        ((TransactionEntity::Ourself, token2), -(value2 as i64)),
+
+    assert_eq!(
+        &report.totals,
+        &[
+            // Bob's spend to create the transaction
+            (token2, value2 as i64),
+        ]
+    );
+    let mut outputs = vec![
+        // Converted output to charlie, - fee paid from Mob input
         (
-            (TransactionEntity::Address(charlie_hash), Mob::ID),
-            ((value - Mob::MINIMUM_FEE) as i64),
+            TransactionEntity::OtherAddress(charlie_hash),
+            Mob::ID,
+            (value - Mob::MINIMUM_FEE),
         ),
-        ((TransactionEntity::Swap, Mob::ID), -(value as i64)),
-        ((TransactionEntity::Swap, token2), (value2 as i64)),
+        // Output to swap counterparty
+        (TransactionEntity::Swap, token2, value2),
     ];
-    expected.sort();
+    outputs.sort();
+    assert_eq!(&report.outputs[..], &outputs[..]);
 
-    assert_eq!(balance_changes, expected);
     assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, Mob::ID));
 }
diff --git a/transaction/summary/Cargo.toml b/transaction/summary/Cargo.toml
index b8da40410c..72e022e561 100644
--- a/transaction/summary/Cargo.toml
+++ b/transaction/summary/Cargo.toml
@@ -27,6 +27,7 @@ default = ["std", "serde", "prost", "mc-account-keys"]
 [dependencies]
 # External dependencies
 displaydoc = { version = "0.2", default-features = false }
+heapless = { version = "0.7.16", default-features = false }
 
 # MobileCoin dependencies
 mc-account-keys = { path = "../../account-keys", optional = true, default-features = false }
@@ -42,3 +43,8 @@ prost = { version = "0.11", optional = true, default-features = false, features
 serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
 subtle = { version = "2.4.1", default-features = false, features = ["i128"] }
 zeroize = { version = "1", default-features = false }
+
+[dev-dependencies]
+mc-transaction-core = { path = "../core", default-features = false, features = [] }
+mc-util-from-random = { path = "../../util/from-random", default-features = false }
+rand = "0.8.5"
diff --git a/transaction/summary/src/data.rs b/transaction/summary/src/data.rs
index 04c569af64..ac3a55a27a 100644
--- a/transaction/summary/src/data.rs
+++ b/transaction/summary/src/data.rs
@@ -3,9 +3,9 @@
 use alloc::vec::Vec;
 
 use super::{Error, TxSummaryUnblindingReport};
-use crate::TxSummaryStreamingVerifierCtx;
+use crate::{report::TransactionReport, TxSummaryStreamingVerifierCtx};
 use mc_account_keys::PublicAddress;
-use mc_core::account::ShortAddressHash;
+use mc_core::account::{PublicSubaddress, RingCtAddress, ShortAddressHash};
 use mc_crypto_digestible::Digestible;
 use mc_crypto_keys::RistrettoPrivate;
 use mc_transaction_types::{Amount, TxSummary, UnmaskedAmount};
@@ -76,6 +76,7 @@ pub fn verify_tx_summary(
     tx_summary: &TxSummary,
     unblinding_data: &TxSummaryUnblindingData,
     view_private_key: RistrettoPrivate,
+    change_address: impl RingCtAddress,
 ) -> Result<([u8; 32], TxSummaryUnblindingReport), Error> {
     let mut verifier = TxSummaryStreamingVerifierCtx::new(
         extended_message_digest,
@@ -83,6 +84,10 @@ pub fn verify_tx_summary(
         tx_summary.outputs.len(),
         tx_summary.inputs.len(),
         view_private_key,
+        PublicSubaddress {
+            view_public: change_address.view_public_key(),
+            spend_public: change_address.spend_public_key(),
+        },
     );
     let mut report = TxSummaryUnblindingReport::default();
 
@@ -116,7 +121,9 @@ pub fn verify_tx_summary(
         tx_summary.tombstone_block,
         &mut digest,
         &mut report,
-    );
+    )?;
+
+    report.finalize()?;
 
     // In a debug build, confirm the digest by computing it in a non-streaming way
     //
diff --git a/transaction/summary/src/lib.rs b/transaction/summary/src/lib.rs
index cf6de2c0ba..f035f94f16 100644
--- a/transaction/summary/src/lib.rs
+++ b/transaction/summary/src/lib.rs
@@ -2,7 +2,7 @@
 
 // Copyright (c) 2018-2022 The MobileCoin Foundation
 
-#![no_std]
+#![cfg_attr(not(feature = "std"), no_std)]
 #![doc = include_str!("../README.md")]
 #![deny(missing_docs)]
 
diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index c81d915864..402e9361cd 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -3,90 +3,269 @@
 //! A TxSummaryUnblindingReport, containing the set of verified information
 //! about a transaction.
 
-use super::Error;
 use core::fmt::Display;
+
 use displaydoc::Display;
+use heapless::Vec;
 
 use mc_core::account::ShortAddressHash;
 use mc_transaction_types::{
     constants::{MAX_INPUTS, MAX_OUTPUTS},
     Amount, TokenId,
 };
-use mc_util_vec_map::VecMap;
+
+use super::Error;
 
 /// An entity with whom a transaction can interact, and who can be identified
 /// by the TxSummary verification process
 #[derive(Clone, Debug, Display, Eq, Ord, PartialEq, PartialOrd)]
 pub enum TransactionEntity {
-    /// Self
-    Ourself,
-    /// Address hash {0}
-    Address(ShortAddressHash),
+    /// Outputs to a non-change address that we control (hash {0})
+    OurAddress(ShortAddressHash),
+
+    /// Outputs to other accounts (hash {0})
+    OtherAddress(ShortAddressHash),
+
     /// Swap counterparty
     Swap,
 }
 
+/// Generic transaction report interface
+// (There is at this time only one report implementation, however, this trait
+// is particularly useful for eliding generics when using this and is expected
+// to be helpful when building support for account info caching.)
+pub trait TransactionReport {
+    /// Add value to the transaction running transaction totals
+    fn total_add(&mut self, amount: Amount) -> Result<(), Error>;
+
+    /// Add matched change outputs to the report, these are subtracted from the
+    /// transaction totals
+    fn change_add(&mut self, amount: Amount) -> Result<(), Error>;
+
+    /// Add output value for a particular entity / address to the report
+    fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error>;
+
+    /// Set the network fee
+    fn network_fee_set(&mut self, amount: Amount) -> Result<(), Error>;
+
+    /// Set the tombstone block
+    fn tombstone_block_set(&mut self, value: u64) -> Result<(), Error>;
+
+    /// Finalise the report, checking balances and sorting report entries
+    fn finalize(&mut self) -> Result<(), Error>;
+}
+
+/// [TransactionReport] impl for `&mut T` where `T: TransactionReport`
+impl<T: TransactionReport> TransactionReport for &mut T {
+    fn total_add(&mut self, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::total_add(self, amount)
+    }
+
+    fn change_add(&mut self, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::change_add(self, amount)
+    }
+
+    fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::output_add(self, entity, amount)
+    }
+
+    fn network_fee_set(&mut self, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::network_fee_set(self, amount)
+    }
+
+    fn tombstone_block_set(&mut self, value: u64) -> Result<(), Error> {
+        <T as TransactionReport>::tombstone_block_set(self, value)
+    }
+
+    fn finalize(&mut self) -> Result<(), Error> {
+        <T as TransactionReport>::finalize(self)
+    }
+}
+
+/// Compute maximum number of outputs and inputs to be supported by a report
 pub const MAX_RECORDS: usize = MAX_OUTPUTS as usize + MAX_INPUTS as usize;
 
-/// A report of the parties and balance changes due to a transaction.
-/// This can be produced for a given TxSummary and TxSummaryUnblindingData.
+/// Maximum number of currencies with totals supported in a single report.
+///
+/// It is expected that _most_ transactions will contain one total, however,
+/// this should be large enough to support SCIs with other token types
+pub const MAX_TOTALS: usize = 4;
+
+/// A report of the parties and balance changes due to a transaction,
+/// produced for a given TxSummary and TxSummaryUnblindingData.
+///
+/// This uses a double-entry approach where outputs and totals should be
+/// balanced. For each token, totals = our inputs - sum(change outputs) ==
+/// sum(other outputs) + fee
 #[derive(Clone, Debug, Default)]
-pub struct TxSummaryUnblindingReport<const RECORDS: usize = MAX_RECORDS> {
-    /// The set of balance changes that we have observed
-    // Note: We can save about 210 bytes on the stack if we store TokenId as
-    // a [u8; 8] to avoid alignment requirements. TBD if that's worth it.
-    pub balance_changes: VecMap<(TransactionEntity, TokenId), i64, RECORDS>,
-    /// The network fee that we pay
+pub struct TxSummaryUnblindingReport<
+    const RECORDS: usize = MAX_RECORDS,
+    const TOTALS: usize = MAX_TOTALS,
+> {
+    /// Transaction outputs aggregated by address and token type
+    pub outputs: Vec<(TransactionEntity, TokenId, u64), RECORDS>,
+
+    /// Total balance change for our account for each type of token in the
+    /// transaction.
+    ///
+    /// totals = our inputs - sum(change outputs)
+    ///
+    /// Note that swap inputs are elided as these are not inputs
+    /// owned by us (ie. are not spent from our account)
+    pub totals: Vec<(TokenId, i64), TOTALS>,
+
+    /// The network fee that we pay to execute the transaction
     pub network_fee: Amount,
+
     /// The tombstone block associated to this transaction
     pub tombstone_block: u64,
 }
 
-impl<const RECORDS: usize> TxSummaryUnblindingReport<RECORDS> {
-    /// Add value to the balance report, for some entity
-    pub fn balance_add(
-        &mut self,
-        entity: TransactionEntity,
-        token_id: TokenId,
-        value: u64,
-    ) -> Result<(), Error> {
+impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
+    for TxSummaryUnblindingReport<RECORDS, TOTALS>
+{
+    /// Add input, added to the transaction total
+    fn total_add(&mut self, amount: Amount) -> Result<(), Error> {
+        let Amount { token_id, value } = amount;
+
+        // Ensure value will not overflow
         let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
-        let stored = self
-            .balance_changes
-            .get_mut_or_insert_with(&(entity, token_id), || 0)
-            .map_err(|_| Error::BufferOverflow)?;
-        *stored = stored.checked_add(value).ok_or(Error::NumericOverflow)?;
+
+        // Check for existing total entry for this token
+        match self.totals.iter_mut().find(|(t, _)| t == &token_id) {
+            // If we have an entry, add the value to this
+            Some(v) => v.1 = v.1.checked_add(value).ok_or(Error::NumericOverflow)?,
+            // If we do not, create a new entry
+            None => self
+                .totals
+                .push((token_id, value))
+                .map_err(|_| Error::BufferOverflow)?,
+        }
+
         Ok(())
     }
 
-    /// Subtract value from the balance report, for some entity
-    pub fn balance_subtract(
-        &mut self,
-        entity: TransactionEntity,
-        token_id: TokenId,
-        value: u64,
-    ) -> Result<(), Error> {
+    /// Add change output, subtracted from the transaction total
+    fn change_add(&mut self, amount: Amount) -> Result<(), Error> {
+        let Amount { token_id, value } = amount;
+
+        // Ensure value will not overflow
         let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
-        let stored = self
-            .balance_changes
-            .get_mut_or_insert_with(&(entity, token_id), || 0)
-            .map_err(|_| Error::BufferOverflow)?;
-        *stored = stored.checked_sub(value).ok_or(Error::NumericOverflow)?;
+
+        // Check for existing total entry for this token
+        match self.totals.iter_mut().find(|(t, _)| t == &token_id) {
+            // If we have an entry, subtract the change value from this
+            Some(v) => v.1 = v.1.checked_sub(value).ok_or(Error::NumericOverflow)?,
+            // If we do not, create a new entry
+            None => self
+                .totals
+                .push((token_id, -value))
+                .map_err(|_| Error::BufferOverflow)?,
+        }
+
+        Ok(())
+    }
+
+    /// Add output value to a particular entity / address to the report
+    fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error> {
+        let Amount { token_id, value } = amount;
+
+        // Check for existing output for this address
+        match self
+            .outputs
+            .iter_mut()
+            .find(|(e, t, _)| t == &token_id && e == &entity)
+        {
+            // If we have an entry, subtract the change value from this
+            Some((_, _, v)) => *v = v.checked_add(value).ok_or(Error::NumericOverflow)?,
+            // If we do not, create a new entry
+            None => self
+                .outputs
+                .push((entity, token_id, value))
+                .map_err(|_| Error::BufferOverflow)?,
+        }
+
         Ok(())
     }
 
-    /// This should be done before displaying the report
+    /// Add network fee to the report
+    fn network_fee_set(&mut self, amount: Amount) -> Result<(), Error> {
+        // Set fee value
+        self.network_fee = amount;
+
+        Ok(())
+    }
+
+    /// Set tombstone block in the report
+    fn tombstone_block_set(&mut self, value: u64) -> Result<(), Error> {
+        self.tombstone_block = value;
+        Ok(())
+    }
+
+    /// Finalise report, checking totals and sorting report entries
+    fn finalize(&mut self) -> Result<(), Error> {
+        // Sort outputs and totals
+        self.sort();
+
+        // For each token id, check that inputs match outputs
+        for (token_id, value) in &self.totals {
+            // Sum outputs for this token id
+            let mut balance = 0u64;
+            for (_e, id, v) in &self.outputs {
+                if id == token_id {
+                    balance = balance.checked_add(*v).ok_or(Error::NumericOverflow)?;
+                }
+            }
+
+            // Add network fee for matching token id
+            if &self.network_fee.token_id == token_id {
+                balance = balance
+                    .checked_add(self.network_fee.value)
+                    .ok_or(Error::NumericOverflow)?;
+            }
+
+            // Check that the balance matches the total
+            if balance != *value as u64 {
+                return Err(Error::AmountVerificationFailed);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl<const RECORDS: usize, const TOTALS: usize> TxSummaryUnblindingReport<RECORDS, TOTALS> {
+    /// Create a new report instance
+    pub fn new() -> Self {
+        Self {
+            outputs: Vec::new(),
+            totals: Vec::new(),
+            network_fee: Default::default(),
+            tombstone_block: 0,
+        }
+    }
+
+    /// Sort balance changes and totals
+    ///
+    /// This should be called prior to displaying the report.
     pub fn sort(&mut self) {
-        self.balance_changes.sort();
+        // TODO: should we remove zeroed balances / totals?
+
+        (&mut self.outputs[..]).sort_by_key(|(_, t, _)| *t);
+        (&mut self.outputs[..]).sort_by_key(|(e, _, _)| e.clone());
+
+        (&mut self.totals[..]).sort_by_key(|(t, _)| *t);
     }
 }
 
 // This is a proof-of-concept, it doesn't map token id's to their symbol when
 // displaying.
-impl<const RECORDS: usize> Display for TxSummaryUnblindingReport<RECORDS> {
+impl<const RECORDS: usize, const TOTALS: usize> Display
+    for TxSummaryUnblindingReport<RECORDS, TOTALS>
+{
     fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
         let mut current_entity = None;
-        for ((entity, tok), val) in self.balance_changes.iter() {
+        for (entity, tok, val) in self.outputs.iter() {
             if Some(entity) != current_entity.as_ref() {
                 writeln!(formatter, "{entity}:")?;
                 current_entity = Some(entity.clone());
@@ -104,9 +283,85 @@ impl<const RECORDS: usize> Display for TxSummaryUnblindingReport<RECORDS> {
 
 #[cfg(test)]
 mod tests {
+    use rand::random;
+
     use super::*;
+
     #[test]
-    fn test_report_size_size() {
-        assert_eq!(core::mem::size_of::<TxSummaryUnblindingReport>(), 1320);
+    fn test_report_size() {
+        assert_eq!(core::mem::size_of::<TxSummaryUnblindingReport>(), 1384);
+    }
+
+    #[test]
+    fn test_report_totals() {
+        let mut report = TxSummaryUnblindingReport::<16>::new();
+
+        let amounts = [
+            Amount::new(50, TokenId::from(1)),
+            Amount::new(50, TokenId::from(1)),
+            Amount::new(100, TokenId::from(2)),
+            Amount::new(200, TokenId::from(2)),
+        ];
+
+        for a in amounts {
+            report.total_add(a).unwrap();
+        }
+
+        // Check total inputs
+        report.sort();
+        assert_eq!(
+            &report.totals[..],
+            &[(TokenId::from(1), 100), (TokenId::from(2), 300)]
+        );
+
+        // Subtract change amounts
+        report
+            .change_add(Amount::new(25, TokenId::from(1)))
+            .unwrap();
+        report
+            .change_add(Amount::new(50, TokenId::from(2)))
+            .unwrap();
+
+        // Check total inputs - change
+        assert_eq!(
+            &report.totals[..],
+            &[(TokenId::from(1), 75), (TokenId::from(2), 250)]
+        );
+    }
+
+    #[test]
+    fn test_report_balances() {
+        let mut report = TxSummaryUnblindingReport::<16>::new();
+
+        // Setup random addresses, sorted so these match the report entry order
+        let mut addrs = [
+            TransactionEntity::OtherAddress(ShortAddressHash::from(random::<[u8; 16]>())),
+            TransactionEntity::OtherAddress(ShortAddressHash::from(random::<[u8; 16]>())),
+        ];
+        addrs.sort();
+
+        let amounts = [
+            (addrs[0].clone(), Amount::new(50, TokenId::from(1))),
+            (addrs[0].clone(), Amount::new(50, TokenId::from(1))),
+            (addrs[0].clone(), Amount::new(80, TokenId::from(2))),
+            (addrs[1].clone(), Amount::new(120, TokenId::from(2))),
+            (TransactionEntity::Swap, Amount::new(200, TokenId::from(2))),
+        ];
+
+        for (e, a) in amounts {
+            report.output_add(e, a).unwrap();
+        }
+
+        // Check total outputs
+        report.sort();
+        assert_eq!(
+            &report.outputs[..],
+            &[
+                (addrs[0].clone(), TokenId::from(1), 100),
+                (addrs[0].clone(), TokenId::from(2), 80),
+                (addrs[1].clone(), TokenId::from(2), 120),
+                (TransactionEntity::Swap, TokenId::from(2), 200),
+            ]
+        );
     }
 }
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index 2c028f03bb..17810070a4 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -10,8 +10,10 @@
 //! To take the largest "step" (verifying an output) requires
 //! approximately 300 bytes + Fog url length
 
-use super::{Error, TransactionEntity, TxSummaryUnblindingReport};
-use mc_core::account::{RingCtAddress, ShortAddressHash};
+use crate::report::TransactionReport;
+
+use super::{Error, TransactionEntity};
+use mc_core::account::{PublicSubaddress, RingCtAddress, ShortAddressHash};
 use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript};
 use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic};
 use mc_crypto_ring_signature::{
@@ -51,6 +53,10 @@ pub struct TxSummaryStreamingVerifierCtx {
     // The account view private key of the transaction signer.
     // This is used to identify outputs addressed to ourselves regardless of subaddress
     view_private_key: RistrettoPrivate,
+
+    // The account change address for matching outputs
+    change_address: PublicSubaddress,
+
     // The block version that this transaction is targetting
     block_version: BlockVersion,
     // The merlin transcript which we maintain in order to produce the digest
@@ -87,6 +93,7 @@ impl TxSummaryStreamingVerifierCtx {
         expected_num_outputs: usize,
         expected_num_inputs: usize,
         view_private_key: RistrettoPrivate,
+        change_address: PublicSubaddress,
     ) -> Self {
         let mut transcript =
             MerlinTranscript::new(EXTENDED_MESSAGE_AND_TX_SUMMARY_DOMAIN_TAG.as_bytes());
@@ -105,18 +112,19 @@ impl TxSummaryStreamingVerifierCtx {
             expected_num_inputs,
             output_count: 0,
             input_count: 0,
+            change_address,
         }
     }
 
     /// Stream the next TxOutSummary and matching unblinding data to the
     /// streaming verifier, which will verify and then digest it.
-    pub fn digest_output<const N: usize>(
+    pub fn digest_output(
         &mut self,
         tx_out_summary: &TxOutSummary,
         unmasked_amount: &UnmaskedAmount,
         address: Option<(ShortAddressHash, impl RingCtAddress)>,
         tx_private_key: Option<&RistrettoPrivate>,
-        report: &mut TxSummaryUnblindingReport<N>,
+        mut report: impl TransactionReport,
     ) -> Result<(), Error> {
         if self.output_count >= self.expected_num_outputs {
             return Err(Error::UnexpectedOutput);
@@ -124,10 +132,33 @@ impl TxSummaryStreamingVerifierCtx {
 
         // Now try to verify the recipient. This is either ourselves, or someone else
         // with the listed address, or this is associated to an SCI.
+
+        // If we view-key matched the output, then it belongs to one of our subaddresses
         if let Some(amount) = self.view_key_match(tx_out_summary)? {
-            // If we view-key matched the output, then it belongs to one of our subaddresses
-            report.balance_add(TransactionEntity::Ourself, amount.token_id, amount.value)?;
+            // If we have address information
+            if let Some((address_hash, address)) = address.as_ref() {
+                // Check whether this is to our change address
+                if address.view_public_key() == self.change_address.view_public_key()
+                    && address.spend_public_key() == self.change_address.spend_public_key()
+                {
+                    // If this is to our change address, subtract this from the total inputs
+                    report.change_add(amount)?;
+                } else {
+                    // Otherwise, add this as an output to ourself
+                    report
+                        .output_add(TransactionEntity::OurAddress(address_hash.clone()), amount)?;
+                }
+            } else {
+                // TODO: If we _don't_ have address information but it's to our own address...
+                // what then? is this even possible??!
+                panic!("what's the right thing to do here..?");
+            }
+
+        // If we didn't match the output, and we have address information, this
+        // belongs to someone else
         } else if let Some((address_hash, address)) = address.as_ref() {
+            // Otherwise, this belongs to another address
+
             let amount = Amount::new(unmasked_amount.value, unmasked_amount.token_id.into());
             // In this case, we are given the address of who is supposed to have received
             // this.
@@ -136,14 +167,17 @@ impl TxSummaryStreamingVerifierCtx {
             let expected =
                 Self::expected_tx_out_summary(self.block_version, amount, address, tx_private_key)?;
             if &expected == tx_out_summary {
-                report.balance_add(
-                    TransactionEntity::Address(address_hash.clone()),
-                    amount.token_id,
-                    amount.value,
+                // Add as an output to the report
+                report.output_add(
+                    TransactionEntity::OtherAddress(address_hash.clone()),
+                    amount,
                 )?;
             } else {
                 return Err(Error::AddressVerificationFailed);
             }
+
+        // If we didn't match the output, and we don't have address information,
+        // this is an SCI
         } else {
             if !tx_out_summary.associated_to_input_rules {
                 return Err(Error::MissingDataRequiredToVerifyTxOutRecipient);
@@ -165,7 +199,12 @@ impl TxSummaryStreamingVerifierCtx {
             {
                 return Err(Error::AmountVerificationFailed);
             }
-            report.balance_add(TransactionEntity::Swap, token_id.into(), value)?;
+
+            // Add swap output to report
+            // NOTE: this is not exercised as swap rings are created without a transaction
+            // ... does this mean the ocurrence of a swap output is invalid / doesn't need
+            // to be handled here?
+            report.output_add(TransactionEntity::Swap, unmasked_amount.into())?;
         }
 
         // We've now verified the tx_out_summary and added it to the report.
@@ -186,11 +225,11 @@ impl TxSummaryStreamingVerifierCtx {
 
     /// Stream the next TxInSummary and matching unblinding data to the
     /// streaming verifier, which will verify and then digest it.
-    pub fn digest_input<const N: usize>(
+    pub fn digest_input(
         &mut self,
         tx_in_summary: &TxInSummary,
         tx_in_summary_unblinding_data: &UnmaskedAmount,
-        report: &mut TxSummaryUnblindingReport<N>,
+        mut report: impl TransactionReport,
     ) -> Result<(), Error> {
         if self.output_count != self.expected_num_outputs {
             return Err(Error::StillExpectingMoreOutputs);
@@ -211,14 +250,15 @@ impl TxSummaryStreamingVerifierCtx {
         }
 
         // Now understand whose input this is. There are two cases
-        let entity = if tx_in_summary.input_rules_digest.is_empty() {
-            TransactionEntity::Ourself
+        if tx_in_summary.input_rules_digest.is_empty() {
+            // If we have no input rules digest, then this is a normal input
+            // add this to the report total
+            report.total_add(tx_in_summary_unblinding_data.into())?;
         } else {
-            TransactionEntity::Swap
+            // If we have input rules this is an SCI input and does not impact
+            // our balance
         };
 
-        report.balance_subtract(entity, token_id.into(), value)?;
-
         // We've now verified the tx_in_summary and added it to the report.
         // Now we need to add it to the digest
         // (See mc-crypto-digestible sources for details around b"")
@@ -239,16 +279,15 @@ impl TxSummaryStreamingVerifierCtx {
     /// * extended-message-and-tx-summary digest
     /// * TxSummaryUnblindingReport, which details all balance changes for all
     ///   parties to this Tx.
-    pub fn finalize<const N: usize>(
+    pub fn finalize(
         mut self,
         fee: Amount,
         tombstone_block: u64,
         digest: &mut [u8; 32],
-        report: &mut TxSummaryUnblindingReport<N>,
-    ) {
-        report.network_fee = fee;
-        report.tombstone_block = tombstone_block;
-        report.sort();
+        mut report: impl TransactionReport,
+    ) -> Result<(), Error> {
+        report.network_fee_set(fee)?;
+        report.tombstone_block_set(tombstone_block)?;
 
         fee.value.append_to_transcript(b"fee", &mut self.transcript);
         (*fee.token_id).append_to_transcript(b"fee_token_id", &mut self.transcript);
@@ -260,6 +299,8 @@ impl TxSummaryStreamingVerifierCtx {
 
         // Extract the digest
         self.transcript.extract_digest(digest);
+
+        Ok(())
     }
 
     // Internal: Check if TxOutSummary matches to our view private key
@@ -321,17 +362,328 @@ impl TxSummaryStreamingVerifierCtx {
 mod tests {
     use super::*;
 
+    use alloc::{vec, vec::Vec};
+
+    use rand::rngs::OsRng;
+
+    use crate::TxSummaryUnblindingReport;
+    use mc_account_keys::AccountKey;
+    use mc_transaction_core::{tx::TxOut, BlockVersion};
+    use mc_transaction_types::TokenId;
+    use mc_util_from_random::FromRandom;
+
     // Test the size of the streaming verifier on the stack. This is using heapless.
     #[test]
     fn test_streaming_verifier_size() {
         let s = core::mem::size_of::<TxSummaryStreamingVerifierCtx>();
         assert!(
-            s < 512,
+            s < 1024,
             "TxSummaryStreamingVerifierCtx exceeds size thresold {}/{}",
             s,
-            512
+            1024
         );
     }
 
-    // Note: Most tests are in transaction/extra/tests to avoid build issues.
+    #[derive(Clone, Debug, PartialEq)]
+    struct TxOutReportTest {
+        /// Inputs spent in the transaction
+        inputs: Vec<(InputType, Amount)>,
+        /// Outputs produced by the transaction
+        outputs: Vec<(OutputTarget, Amount)>,
+        /// Totals / balances by token
+        totals: Vec<(TokenId, i64)>,
+        /// Changes produced by the transaction
+        changes: Vec<(TransactionEntity, TokenId, u64)>,
+    }
+
+    #[derive(Clone, Debug, PartialEq)]
+    #[allow(dead_code)]
+    enum InputType {
+        /// An input we own, reducing our balance
+        Owned,
+        /// A SCI / SWAP input from another account
+        Sci,
+    }
+
+    #[derive(Clone, Debug, PartialEq)]
+    #[allow(dead_code)]
+    enum OutputTarget {
+        /// An output to ourself (_not_ a change address)
+        Ourself,
+        /// An output to our change address
+        Change,
+        /// An output to a third party
+        Other,
+        /// A swap output (not used in existing reports)
+        Swap,
+    }
+
+    #[test]
+    fn test_report_outputs() {
+        let mut rng = OsRng {};
+
+        // Setup accounts for test report
+        let sender = AccountKey::random(&mut rng);
+        let receiver = AccountKey::random(&mut rng);
+        let swap = AccountKey::random(&mut rng);
+
+        let sender_subaddress = sender.default_subaddress();
+        let change_subaddress = sender.change_subaddress();
+        let target_subaddress = receiver.default_subaddress();
+        let swap_subaddress = swap.default_subaddress();
+
+        // Set common token id / amounts for later use
+        let token_id = TokenId::from(9);
+        let amount = Amount::new(103_000, token_id);
+        let fee = 4000;
+
+        // Setup tests
+        let tests = &[
+            // Output to ourself, should show output to our address and total of output + fee
+            TxOutReportTest {
+                inputs: vec![(InputType::Owned, Amount::new(amount.value + fee, token_id))],
+                outputs: vec![(OutputTarget::Ourself, amount.clone())],
+                changes: vec![(
+                    TransactionEntity::OurAddress(ShortAddressHash::from(&sender_subaddress)),
+                    token_id,
+                    amount.value,
+                )],
+                totals: vec![(token_id, (amount.value + fee) as i64)],
+            },
+            // Output to our change address, should show no outputs with balance change = fee
+            TxOutReportTest {
+                inputs: vec![
+                    (
+                        InputType::Owned,
+                        Amount::new(amount.value / 2 + fee, token_id),
+                    ),
+                    (InputType::Owned, Amount::new(amount.value / 2, token_id)),
+                ],
+                outputs: vec![(OutputTarget::Change, amount.clone())],
+                changes: vec![
+                    //(TransactionEntity::Total, token_id, 0),
+                ],
+                totals: vec![(token_id, fee as i64)],
+            },
+            // Output to someone else, should show their address and total of output + fee
+            TxOutReportTest {
+                inputs: vec![(InputType::Owned, Amount::new(amount.value + fee, token_id))],
+                outputs: vec![(OutputTarget::Other, amount.clone())],
+                changes: vec![(
+                    TransactionEntity::OtherAddress(ShortAddressHash::from(&target_subaddress)),
+                    token_id,
+                    amount.value,
+                )],
+                totals: vec![(token_id, (amount.value + fee) as i64)],
+            },
+            // Basic SCI. consuming entire swap, inputs should not count towards totals
+            TxOutReportTest {
+                inputs: vec![
+                    // Our input, sent to SCI
+                    (InputType::Owned, Amount::new(10_000 + fee, token_id)),
+                    // SCI input, sent to us
+                    (InputType::Sci, Amount::new(200, TokenId::from(2))),
+                ],
+                outputs: vec![
+                    // We send the converted token to ourself
+                    (OutputTarget::Ourself, Amount::new(200, TokenId::from(2))),
+                    // While fulfilling the requirements of the SCI
+                    (OutputTarget::Swap, Amount::new(10_000, token_id)),
+                ],
+                changes: vec![
+                    (
+                        TransactionEntity::OurAddress(ShortAddressHash::from(&sender_subaddress)),
+                        TokenId::from(2),
+                        200,
+                    ),
+                    (TransactionEntity::Swap, token_id, 10_000),
+                ],
+                totals: vec![
+                    // The total is the change to _our_ balance spent during the transaction
+                    (token_id, (10_000 + fee) as i64),
+                ],
+            },
+            // Partial SCI
+            TxOutReportTest {
+                inputs: vec![
+                    // Our input, owned by us
+                    (InputType::Owned, Amount::new(7_500 + fee, token_id)),
+                    // SCI input, owned by counterparty
+                    (InputType::Sci, Amount::new(200, TokenId::from(2))),
+                ],
+                outputs: vec![
+                    // We send part of the converted token to ourself
+                    (OutputTarget::Ourself, Amount::new(150, TokenId::from(2))),
+                    // Returning the remaining portion to the swap counterparty
+                    (OutputTarget::Swap, Amount::new(50, TokenId::from(2))),
+                    // While fulfilling the requirements of the SCI
+                    (OutputTarget::Swap, Amount::new(7_500, token_id)),
+                ],
+                changes: vec![
+                    (
+                        TransactionEntity::OurAddress(ShortAddressHash::from(&sender_subaddress)),
+                        TokenId::from(2),
+                        150,
+                    ),
+                    (TransactionEntity::Swap, TokenId::from(2), 50),
+                    (TransactionEntity::Swap, token_id, 7_500),
+                ],
+                totals: vec![
+                    // The total is the change to _our_ balance spent during the transaction
+                    (token_id, (7_500 + fee) as i64),
+                ],
+            },
+        ];
+
+        // Run tests
+        for t in tests {
+            println!("Running test: {:?}", t);
+
+            // Setup verifier
+            let mut report = TxSummaryUnblindingReport::<16>::default();
+            let mut verifier = TxSummaryStreamingVerifierCtx::new(
+                &[0u8; 32],
+                BlockVersion::THREE,
+                t.outputs.len(),
+                t.inputs.len(),
+                sender.view_private_key().clone(),
+                PublicSubaddress {
+                    view_public: change_subaddress.view_public_key().clone().into(),
+                    spend_public: change_subaddress.spend_public_key().clone().into(),
+                },
+            );
+
+            // Build and process TxOuts
+            for (target, amount) in &t.outputs {
+                println!("Add output {:?}: {:?}", target, amount);
+
+                // Select target address
+                let receive_subaddress = match target {
+                    OutputTarget::Ourself => &sender_subaddress,
+                    OutputTarget::Change => &change_subaddress,
+                    OutputTarget::Other => &target_subaddress,
+                    OutputTarget::Swap => &swap_subaddress,
+                };
+
+                // Setup keys for TxOut
+                let tx_private_key = RistrettoPrivate::from_random(&mut rng);
+                let txout_shared_secret =
+                    create_shared_secret(receive_subaddress.view_public_key(), &tx_private_key);
+
+                // Construct TxOut object
+                let tx_out = TxOut::new(
+                    BlockVersion::THREE,
+                    amount.clone(),
+                    &receive_subaddress,
+                    &tx_private_key,
+                    Default::default(),
+                )
+                .unwrap();
+
+                // Build TxOut unblinding
+                let masked_amount = tx_out.get_masked_amount().unwrap();
+                let (amount, blinding) = masked_amount.get_value(&txout_shared_secret).unwrap();
+                let unmasked_amount = UnmaskedAmount {
+                    value: amount.value,
+                    token_id: *amount.token_id,
+                    blinding: blinding.into(),
+                };
+
+                // Build TxOut summary
+                let target_key = create_tx_out_target_key(&tx_private_key, receive_subaddress);
+                let tx_out_summary = TxOutSummary {
+                    masked_amount: Some(masked_amount.clone()),
+                    target_key: target_key.into(),
+                    public_key: tx_out.public_key,
+                    associated_to_input_rules: target == &OutputTarget::Swap,
+                };
+
+                // Set address for normal outputs or SCIs
+                // TODO: do we _never_ have the output address for an SCI?
+                let address = match target != &OutputTarget::Swap {
+                    true => Some((
+                        ShortAddressHash::from(receive_subaddress),
+                        receive_subaddress,
+                    )),
+                    false => None,
+                };
+
+                // Digest TxOout + Summary with verifier
+                verifier
+                    .digest_output(
+                        &tx_out_summary,
+                        &unmasked_amount,
+                        address,
+                        Some(&tx_private_key),
+                        &mut report,
+                    )
+                    .unwrap();
+            }
+
+            // Build and process TxIns?
+            for (kind, amount) in &t.inputs {
+                println!("Add input: {:?}", amount);
+
+                // Setup keys for TxOut (kx against sender key as this is an input)
+                let tx_private_key = RistrettoPrivate::from_random(&mut rng);
+                let txout_shared_secret =
+                    create_shared_secret(sender_subaddress.view_public_key(), &tx_private_key);
+
+                // Construct TxOut object
+                let tx_out = TxOut::new(
+                    BlockVersion::THREE,
+                    amount.clone(),
+                    &sender_subaddress,
+                    &tx_private_key,
+                    Default::default(),
+                )
+                .unwrap();
+
+                let masked_amount = tx_out.get_masked_amount().unwrap();
+
+                // Build TxIn summary
+                let input_rules_digest = match kind {
+                    InputType::Owned => Vec::new(),
+                    InputType::Sci => vec![0u8; 32],
+                };
+                let tx_in_summary = TxInSummary {
+                    pseudo_output_commitment: masked_amount.commitment().clone(),
+                    input_rules_digest,
+                };
+
+                // Build TxIn unblinding
+                let (amount, blinding) = masked_amount.get_value(&txout_shared_secret).unwrap();
+                let unmasked_amount = UnmaskedAmount {
+                    value: amount.value,
+                    token_id: *amount.token_id,
+                    blinding: blinding.into(),
+                };
+
+                // Digest transaction input
+                verifier
+                    .digest_input(&tx_in_summary, &unmasked_amount, &mut report)
+                    .unwrap();
+            }
+
+            // Finalize verifier
+            let mut digest = [0u8; 32];
+            verifier
+                .finalize(Amount::new(fee, token_id), 1234, &mut digest, &mut report)
+                .unwrap();
+
+            report.finalize().unwrap();
+
+            // Check report totals
+            let totals: Vec<_> = report.totals.iter().map(|(t, v)| (*t, *v)).collect();
+            assert_eq!(&totals, &t.totals, "Total mismatch");
+
+            // Check report outputs
+            let changes: Vec<_> = report
+                .outputs
+                .iter()
+                .map(|(e, t, v)| (e.clone(), *t, *v))
+                .collect();
+            assert_eq!(&changes, &t.changes, "Output mismatch");
+        }
+    }
 }

From 28470de81b6a62a6d90efb24aad20e2bf222ca07 Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Wed, 21 Jun 2023 13:44:11 +1200
Subject: [PATCH 02/11] add sci inputs to summary reports

---
 Cargo.lock                          | 218 +++++++++++++++++++---------
 transaction/summary/src/report.rs   | 145 +++++++++++++-----
 transaction/summary/src/verifier.rs |  35 ++---
 3 files changed, 280 insertions(+), 118 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index d1e7c89c47..1bdb82d072 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -680,30 +680,38 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.1.11"
+version = "4.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
+checksum = "906f7fe1da4185b7a282b2bc90172a496f9def1aca4545fe7526810741591e14"
 dependencies = [
- "bitflags 2.0.2",
+ "clap_builder",
  "clap_derive",
- "clap_lex 0.3.0",
- "is-terminal",
  "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "351f9ad9688141ed83dfd8f5fb998a06225ef444b48ff4dc43de6d409b7fd10b"
+dependencies = [
+ "bitflags 1.3.2",
+ "clap_lex 0.4.1",
+ "is-terminal",
  "strsim 0.10.0",
  "termcolor",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.1.9"
+version = "4.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
+checksum = "81d7dc0031c3a59a04fc2ba395c8e2dd463cba1859275f065d225f6122221b45"
 dependencies = [
  "heck",
- "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.12",
 ]
 
 [[package]]
@@ -717,12 +725,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.3.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
-dependencies = [
- "os_str_bytes",
-]
+checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
 
 [[package]]
 name = "clear_on_drop"
@@ -1160,9 +1165,9 @@ dependencies = [
 
 [[package]]
 name = "dirs"
-version = "4.0.0"
+version = "5.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
 dependencies = [
  "dirs-sys",
 ]
@@ -1179,13 +1184,14 @@ dependencies = [
 
 [[package]]
 name = "dirs-sys"
-version = "0.3.6"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
 dependencies = [
  "libc",
+ "option-ext",
  "redox_users",
- "winapi",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1589,7 +1595,7 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 name = "go-grpc-gateway-testing"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "futures",
  "grpcio",
@@ -2296,7 +2302,7 @@ dependencies = [
 name = "mc-admin-http-gateway"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "mc-common",
  "mc-util-grpc",
@@ -2843,7 +2849,7 @@ dependencies = [
 name = "mc-consensus-mint-client"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "grpcio",
  "hex",
@@ -2915,7 +2921,7 @@ dependencies = [
 name = "mc-consensus-scp-play"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-common",
  "mc-consensus-scp",
  "mc-transaction-core",
@@ -2945,7 +2951,7 @@ version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
  "chrono",
- "clap 4.1.11",
+ "clap 4.1.14",
  "curve25519-dalek",
  "displaydoc",
  "fs_extra",
@@ -3009,7 +3015,7 @@ name = "mc-consensus-service-config"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "hex",
  "mc-attest-core",
@@ -3035,7 +3041,7 @@ dependencies = [
 name = "mc-consensus-tool"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "mc-common",
  "mc-connection",
@@ -3050,7 +3056,7 @@ name = "mc-core"
 version = "5.0.8"
 dependencies = [
  "anyhow",
- "clap 4.1.11",
+ "clap 4.1.14",
  "curve25519-dalek",
  "ed25519-dalek",
  "generic-array",
@@ -3341,7 +3347,7 @@ name = "mc-crypto-x509-test-vectors"
 version = "5.0.8"
 dependencies = [
  "cargo-emit",
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-crypto-keys",
  "mc-util-build-script",
  "pem",
@@ -3407,7 +3413,7 @@ dependencies = [
 name = "mc-fog-distribution"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "crossbeam-channel",
  "curve25519-dalek",
  "grpcio",
@@ -3465,7 +3471,7 @@ name = "mc-fog-ingest-client"
 version = "5.0.8"
 dependencies = [
  "assert_cmd",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "grpcio",
  "hex",
@@ -3620,7 +3626,7 @@ dependencies = [
 name = "mc-fog-ingest-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "dirs",
  "displaydoc",
  "futures",
@@ -3842,7 +3848,7 @@ name = "mc-fog-ledger-server"
 version = "5.0.8"
 dependencies = [
  "aes-gcm",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "futures",
  "grpcio",
@@ -3925,7 +3931,7 @@ dependencies = [
 name = "mc-fog-load-testing"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "mc-account-keys",
  "mc-blockchain-test-utils",
@@ -3997,7 +4003,7 @@ dependencies = [
 name = "mc-fog-overseer-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "grpcio",
  "lazy_static",
@@ -4080,7 +4086,7 @@ name = "mc-fog-report-cli"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "hex",
  "mc-account-keys",
@@ -4135,7 +4141,7 @@ dependencies = [
 name = "mc-fog-report-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "futures",
  "grpcio",
@@ -4203,7 +4209,7 @@ name = "mc-fog-sample-paykit"
 version = "5.0.8"
 dependencies = [
  "cargo-emit",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "futures",
  "grpcio",
@@ -4301,7 +4307,7 @@ name = "mc-fog-sql-recovery-db"
 version = "5.0.8"
 dependencies = [
  "chrono",
- "clap 4.1.11",
+ "clap 4.1.14",
  "diesel",
  "diesel-derive-enum",
  "diesel_migrations",
@@ -4334,7 +4340,7 @@ name = "mc-fog-sql-recovery-db-cleanup"
 version = "5.0.8"
 dependencies = [
  "chrono",
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-common",
  "mc-fog-recovery-db-iface",
  "mc-fog-sql-recovery-db",
@@ -4345,7 +4351,7 @@ dependencies = [
 name = "mc-fog-test-client"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "grpcio",
  "hex_fmt",
@@ -4379,7 +4385,7 @@ dependencies = [
 name = "mc-fog-test-infra"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "digest 0.10.6",
  "hex",
  "mc-account-keys",
@@ -4572,7 +4578,7 @@ dependencies = [
 name = "mc-fog-view-load-test"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "mc-account-keys",
  "mc-attest-verifier",
@@ -4614,7 +4620,7 @@ dependencies = [
 name = "mc-fog-view-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "futures",
  "grpcio",
@@ -4725,7 +4731,7 @@ dependencies = [
 name = "mc-ledger-distribution"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "dirs",
  "displaydoc",
  "mc-api",
@@ -4748,7 +4754,7 @@ dependencies = [
 name = "mc-ledger-from-archive"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-api",
  "mc-common",
  "mc-ledger-db",
@@ -4759,7 +4765,7 @@ dependencies = [
 name = "mc-ledger-migration"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "lmdb-rkv",
  "mc-common",
  "mc-ledger-db",
@@ -4807,7 +4813,7 @@ name = "mc-mobilecoind"
 version = "5.0.8"
 dependencies = [
  "aes-gcm",
- "clap 4.1.11",
+ "clap 4.1.14",
  "crossbeam-channel",
  "displaydoc",
  "grpcio",
@@ -4895,7 +4901,7 @@ name = "mc-mobilecoind-dev-faucet"
 version = "5.0.8"
 dependencies = [
  "async-channel",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "grpcio",
  "hex",
@@ -4926,7 +4932,7 @@ dependencies = [
 name = "mc-mobilecoind-json"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "hex",
  "mc-api",
@@ -5106,7 +5112,7 @@ dependencies = [
 name = "mc-sgx-css-dump"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "hex_fmt",
  "mc-sgx-css",
 ]
@@ -5392,7 +5398,7 @@ name = "mc-transaction-signer"
 version = "5.0.8"
 dependencies = [
  "anyhow",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "hex",
  "log",
@@ -5421,15 +5427,19 @@ name = "mc-transaction-summary"
 version = "5.0.8"
 dependencies = [
  "displaydoc",
+ "heapless",
  "mc-account-keys",
  "mc-core",
  "mc-crypto-digestible",
  "mc-crypto-keys",
  "mc-crypto-ring-signature",
+ "mc-transaction-core",
  "mc-transaction-types",
+ "mc-util-from-random",
  "mc-util-vec-map",
  "mc-util-zip-exact",
  "prost",
+ "rand",
  "serde",
  "subtle",
  "zeroize",
@@ -5458,7 +5468,7 @@ dependencies = [
 name = "mc-util-b58-decoder"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "hex",
  "mc-account-keys",
  "mc-api",
@@ -5522,7 +5532,7 @@ dependencies = [
 name = "mc-util-cli"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-util-build-info",
 ]
 
@@ -5530,7 +5540,7 @@ dependencies = [
 name = "mc-util-dump-ledger"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "mc-blockchain-types",
  "mc-common",
@@ -5565,7 +5575,7 @@ dependencies = [
 name = "mc-util-generate-sample-ledger"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-account-keys",
  "mc-blockchain-test-utils",
  "mc-common",
@@ -5585,7 +5595,7 @@ name = "mc-util-grpc"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.11",
+ "clap 4.1.14",
  "cookie 0.17.0",
  "displaydoc",
  "futures",
@@ -5618,7 +5628,7 @@ dependencies = [
 name = "mc-util-grpc-admin-tool"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "grpcio",
  "mc-common",
  "mc-util-grpc",
@@ -5629,7 +5639,7 @@ dependencies = [
 name = "mc-util-grpc-token-generator"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-common",
  "mc-util-grpc",
  "mc-util-parse",
@@ -5645,7 +5655,7 @@ name = "mc-util-keyfile"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "hex",
  "mc-account-keys",
@@ -5733,7 +5743,7 @@ dependencies = [
 name = "mc-util-seeded-ed25519-key-gen"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "mc-crypto-keys",
  "mc-util-from-random",
  "mc-util-parse",
@@ -5769,7 +5779,7 @@ dependencies = [
 name = "mc-util-test-helper"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "itertools",
  "lazy_static",
  "mc-account-keys",
@@ -5851,7 +5861,7 @@ dependencies = [
 name = "mc-watcher"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.11",
+ "clap 4.1.14",
  "displaydoc",
  "futures",
  "grpcio",
@@ -6239,6 +6249,12 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "ordered-float"
 version = "1.1.1"
@@ -7451,9 +7467,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.159"
+version = "1.0.164"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
 dependencies = [
  "serde_derive",
 ]
@@ -7479,9 +7495,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.159"
+version = "1.0.164"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
+checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -8662,21 +8678,51 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
 dependencies = [
- "windows_aarch64_gnullvm",
+ "windows_aarch64_gnullvm 0.42.0",
  "windows_aarch64_msvc 0.42.0",
  "windows_i686_gnu 0.42.0",
  "windows_i686_msvc 0.42.0",
  "windows_x86_64_gnu 0.42.0",
- "windows_x86_64_gnullvm",
+ "windows_x86_64_gnullvm 0.42.0",
  "windows_x86_64_msvc 0.42.0",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.36.1"
@@ -8689,6 +8735,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.36.1"
@@ -8701,6 +8753,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.36.1"
@@ -8713,6 +8771,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.36.1"
@@ -8725,12 +8789,24 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.36.1"
@@ -8743,6 +8819,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
 [[package]]
 name = "winnow"
 version = "0.4.0"
diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index 402e9361cd..926dbda554 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -35,12 +35,15 @@ pub enum TransactionEntity {
 // is particularly useful for eliding generics when using this and is expected
 // to be helpful when building support for account info caching.)
 pub trait TransactionReport {
-    /// Add value to the transaction running transaction totals
-    fn total_add(&mut self, amount: Amount) -> Result<(), Error>;
+    /// Add value to the running transaction totals
+    fn input_add(&mut self, amount: Amount) -> Result<(), Error>;
 
-    /// Add matched change outputs to the report, these are subtracted from the
-    /// transaction totals
-    fn change_add(&mut self, amount: Amount) -> Result<(), Error>;
+    /// Subtract an amount from the transaction total, used for change outputs
+    /// and SCIs if enabled
+    fn change_sub(&mut self, amount: Amount) -> Result<(), Error>;
+
+    /// Add SCI input not owned by our account
+    fn sci_add(&mut self, _amount: Amount) -> Result<(), Error>;
 
     /// Add output value for a particular entity / address to the report
     fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error>;
@@ -57,12 +60,16 @@ pub trait TransactionReport {
 
 /// [TransactionReport] impl for `&mut T` where `T: TransactionReport`
 impl<T: TransactionReport> TransactionReport for &mut T {
-    fn total_add(&mut self, amount: Amount) -> Result<(), Error> {
-        <T as TransactionReport>::total_add(self, amount)
+    fn input_add(&mut self, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::input_add(self, amount)
+    }
+
+    fn change_sub(&mut self, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::change_sub(self, amount)
     }
 
-    fn change_add(&mut self, amount: Amount) -> Result<(), Error> {
-        <T as TransactionReport>::change_add(self, amount)
+    fn sci_add(&mut self, amount: Amount) -> Result<(), Error> {
+        <T as TransactionReport>::sci_add(self, amount)
     }
 
     fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error> {
@@ -97,6 +104,8 @@ pub const MAX_TOTALS: usize = 4;
 /// This uses a double-entry approach where outputs and totals should be
 /// balanced. For each token, totals = our inputs - sum(change outputs) ==
 /// sum(other outputs) + fee
+///
+/// SCI inputs are currently ignored
 #[derive(Clone, Debug, Default)]
 pub struct TxSummaryUnblindingReport<
     const RECORDS: usize = MAX_RECORDS,
@@ -112,7 +121,7 @@ pub struct TxSummaryUnblindingReport<
     ///
     /// Note that swap inputs are elided as these are not inputs
     /// owned by us (ie. are not spent from our account)
-    pub totals: Vec<(TokenId, i64), TOTALS>,
+    pub totals: Vec<(TokenId, TotalKind, i64), TOTALS>,
 
     /// The network fee that we pay to execute the transaction
     pub network_fee: Amount,
@@ -121,24 +130,36 @@ pub struct TxSummaryUnblindingReport<
     pub tombstone_block: u64,
 }
 
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
+pub enum TotalKind {
+    /// Input owned by our account
+    Ours,
+    /// Input owned by SCI counterparty
+    Sci,
+}
+
 impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
     for TxSummaryUnblindingReport<RECORDS, TOTALS>
 {
-    /// Add input, added to the transaction total
-    fn total_add(&mut self, amount: Amount) -> Result<(), Error> {
+    /// Add owned input, added to the transaction total
+    fn input_add(&mut self, amount: Amount) -> Result<(), Error> {
         let Amount { token_id, value } = amount;
 
         // Ensure value will not overflow
         let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
 
         // Check for existing total entry for this token
-        match self.totals.iter_mut().find(|(t, _)| t == &token_id) {
+        match self
+            .totals
+            .iter_mut()
+            .find(|(t, k, _)| t == &token_id && *k == TotalKind::Ours)
+        {
             // If we have an entry, add the value to this
-            Some(v) => v.1 = v.1.checked_add(value).ok_or(Error::NumericOverflow)?,
+            Some(v) => v.2 = v.2.checked_add(value).ok_or(Error::NumericOverflow)?,
             // If we do not, create a new entry
             None => self
                 .totals
-                .push((token_id, value))
+                .push((token_id, TotalKind::Ours, value))
                 .map_err(|_| Error::BufferOverflow)?,
         }
 
@@ -146,26 +167,54 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
     }
 
     /// Add change output, subtracted from the transaction total
-    fn change_add(&mut self, amount: Amount) -> Result<(), Error> {
+    fn change_sub(&mut self, amount: Amount) -> Result<(), Error> {
         let Amount { token_id, value } = amount;
 
         // Ensure value will not overflow
         let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
 
         // Check for existing total entry for this token
-        match self.totals.iter_mut().find(|(t, _)| t == &token_id) {
+        match self
+            .totals
+            .iter_mut()
+            .find(|(t, k, _)| t == &token_id && *k == TotalKind::Ours)
+        {
             // If we have an entry, subtract the change value from this
-            Some(v) => v.1 = v.1.checked_sub(value).ok_or(Error::NumericOverflow)?,
+            Some(v) => v.2 = v.2.checked_sub(value).ok_or(Error::NumericOverflow)?,
             // If we do not, create a new entry
             None => self
                 .totals
-                .push((token_id, -value))
+                .push((token_id, TotalKind::Ours, -value))
                 .map_err(|_| Error::BufferOverflow)?,
         }
 
         Ok(())
     }
 
+    /// Add SCI (or other) input not owned by our account
+    fn sci_add(&mut self, amount: Amount) -> Result<(), Error> {
+        let Amount { token_id, value } = amount;
+
+        // Ensure value will not overflow
+        let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
+
+        // Check for existing total entry for this token
+        match self
+            .totals
+            .iter_mut()
+            .find(|(t, k, _)| t == &token_id && *k == TotalKind::Sci)
+        {
+            // If we have an entry, add the value to this
+            Some(v) => v.2 = v.2.checked_add(value).ok_or(Error::NumericOverflow)?,
+            // If we do not, create a new entry
+            None => self
+                .totals
+                .push((token_id, TotalKind::Sci, value))
+                .map_err(|_| Error::BufferOverflow)?,
+        }
+        Ok(())
+    }
+
     /// Add output value to a particular entity / address to the report
     fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error> {
         let Amount { token_id, value } = amount;
@@ -202,18 +251,41 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
         Ok(())
     }
 
-    /// Finalise report, checking totals and sorting report entries
+    /// Finalise report, checking and balancing totals and sorting report
+    /// entries
     fn finalize(&mut self) -> Result<(), Error> {
         // Sort outputs and totals
         self.sort();
 
         // For each token id, check that inputs match outputs
-        for (token_id, value) in &self.totals {
+        // (this is only executed where _totals_ exist, so skipped
+        // for the current SCI implementation)
+        for (token_id, total_kind, value) in &mut self.totals {
             // Sum outputs for this token id
             let mut balance = 0u64;
-            for (_e, id, v) in &self.outputs {
-                if id == token_id {
-                    balance = balance.checked_add(*v).ok_or(Error::NumericOverflow)?;
+            for (e, id, v) in &self.outputs {
+                // Skip other tokens
+                if id != token_id {
+                    continue;
+                }
+
+                // Handle balance / values depending on whether the total is from us or a swap
+                // counterparty
+                match total_kind {
+                    // If it's coming from our account, track total balance
+                    TotalKind::Ours => {
+                        balance = balance.checked_add(*v).ok_or(Error::NumericOverflow)?;
+                    }
+                    // If it's coming from an SCI, and returned to the counterparty, reduce total by
+                    // outgoing value
+                    TotalKind::Sci if e == &TransactionEntity::Swap => {
+                        *value = value.checked_sub(*v as i64).ok_or(Error::NumericOverflow)?;
+                    }
+                    // If it's coming from an SCI to us, add to total balance
+                    TotalKind::Sci if e != &TransactionEntity::Swap => {
+                        balance = balance.checked_add(*v).ok_or(Error::NumericOverflow)?;
+                    }
+                    _ => (),
                 }
             }
 
@@ -251,10 +323,11 @@ impl<const RECORDS: usize, const TOTALS: usize> TxSummaryUnblindingReport<RECORD
     pub fn sort(&mut self) {
         // TODO: should we remove zeroed balances / totals?
 
-        (&mut self.outputs[..]).sort_by_key(|(_, t, _)| *t);
-        (&mut self.outputs[..]).sort_by_key(|(e, _, _)| e.clone());
+        self.outputs[..].sort_by_key(|(_, t, _)| *t);
+        self.outputs[..].sort_by_key(|(e, _, _)| e.clone());
 
-        (&mut self.totals[..]).sort_by_key(|(t, _)| *t);
+        self.totals[..].sort_by_key(|(t, _, _)| *t);
+        self.totals[..].sort_by_key(|(_, k, _)| *k);
     }
 }
 
@@ -289,7 +362,7 @@ mod tests {
 
     #[test]
     fn test_report_size() {
-        assert_eq!(core::mem::size_of::<TxSummaryUnblindingReport>(), 1384);
+        assert_eq!(core::mem::size_of::<TxSummaryUnblindingReport>(), 1416);
     }
 
     #[test]
@@ -304,28 +377,34 @@ mod tests {
         ];
 
         for a in amounts {
-            report.total_add(a).unwrap();
+            report.input_add(a).unwrap();
         }
 
         // Check total inputs
         report.sort();
         assert_eq!(
             &report.totals[..],
-            &[(TokenId::from(1), 100), (TokenId::from(2), 300)]
+            &[
+                (TokenId::from(1), TotalKind::Ours, 100),
+                (TokenId::from(2), TotalKind::Ours, 300)
+            ]
         );
 
         // Subtract change amounts
         report
-            .change_add(Amount::new(25, TokenId::from(1)))
+            .change_sub(Amount::new(25, TokenId::from(1)))
             .unwrap();
         report
-            .change_add(Amount::new(50, TokenId::from(2)))
+            .change_sub(Amount::new(50, TokenId::from(2)))
             .unwrap();
 
         // Check total inputs - change
         assert_eq!(
             &report.totals[..],
-            &[(TokenId::from(1), 75), (TokenId::from(2), 250)]
+            &[
+                (TokenId::from(1), TotalKind::Ours, 75),
+                (TokenId::from(2), TotalKind::Ours, 250)
+            ]
         );
     }
 
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index 17810070a4..9eb40b2bcd 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -142,7 +142,7 @@ impl TxSummaryStreamingVerifierCtx {
                     && address.spend_public_key() == self.change_address.spend_public_key()
                 {
                     // If this is to our change address, subtract this from the total inputs
-                    report.change_add(amount)?;
+                    report.change_sub(amount)?;
                 } else {
                     // Otherwise, add this as an output to ourself
                     report
@@ -200,10 +200,7 @@ impl TxSummaryStreamingVerifierCtx {
                 return Err(Error::AmountVerificationFailed);
             }
 
-            // Add swap output to report
-            // NOTE: this is not exercised as swap rings are created without a transaction
-            // ... does this mean the ocurrence of a swap output is invalid / doesn't need
-            // to be handled here?
+            // Add outputs to swap counterparty to the report
             report.output_add(TransactionEntity::Swap, unmasked_amount.into())?;
         }
 
@@ -253,10 +250,11 @@ impl TxSummaryStreamingVerifierCtx {
         if tx_in_summary.input_rules_digest.is_empty() {
             // If we have no input rules digest, then this is a normal input
             // add this to the report total
-            report.total_add(tx_in_summary_unblinding_data.into())?;
+            report.input_add(tx_in_summary_unblinding_data.into())?;
         } else {
             // If we have input rules this is an SCI input and does not impact
-            // our balance
+            // our balance, but we _can_ track this if required
+            report.sci_add(tx_in_summary_unblinding_data.into())?;
         };
 
         // We've now verified the tx_in_summary and added it to the report.
@@ -366,7 +364,7 @@ mod tests {
 
     use rand::rngs::OsRng;
 
-    use crate::TxSummaryUnblindingReport;
+    use crate::{report::TotalKind, TxSummaryUnblindingReport};
     use mc_account_keys::AccountKey;
     use mc_transaction_core::{tx::TxOut, BlockVersion};
     use mc_transaction_types::TokenId;
@@ -391,7 +389,7 @@ mod tests {
         /// Outputs produced by the transaction
         outputs: Vec<(OutputTarget, Amount)>,
         /// Totals / balances by token
-        totals: Vec<(TokenId, i64)>,
+        totals: Vec<(TokenId, TotalKind, i64)>,
         /// Changes produced by the transaction
         changes: Vec<(TransactionEntity, TokenId, u64)>,
     }
@@ -448,7 +446,7 @@ mod tests {
                     token_id,
                     amount.value,
                 )],
-                totals: vec![(token_id, (amount.value + fee) as i64)],
+                totals: vec![(token_id, TotalKind::Ours, (amount.value + fee) as i64)],
             },
             // Output to our change address, should show no outputs with balance change = fee
             TxOutReportTest {
@@ -463,7 +461,7 @@ mod tests {
                 changes: vec![
                     //(TransactionEntity::Total, token_id, 0),
                 ],
-                totals: vec![(token_id, fee as i64)],
+                totals: vec![(token_id, TotalKind::Ours, fee as i64)],
             },
             // Output to someone else, should show their address and total of output + fee
             TxOutReportTest {
@@ -474,7 +472,7 @@ mod tests {
                     token_id,
                     amount.value,
                 )],
-                totals: vec![(token_id, (amount.value + fee) as i64)],
+                totals: vec![(token_id, TotalKind::Ours, (amount.value + fee) as i64)],
             },
             // Basic SCI. consuming entire swap, inputs should not count towards totals
             TxOutReportTest {
@@ -500,7 +498,9 @@ mod tests {
                 ],
                 totals: vec![
                     // The total is the change to _our_ balance spent during the transaction
-                    (token_id, (10_000 + fee) as i64),
+                    (token_id, TotalKind::Ours, (10_000 + fee) as i64),
+                    // And the SCI input
+                    (TokenId::from(2), TotalKind::Sci, (200) as i64),
                 ],
             },
             // Partial SCI
@@ -530,7 +530,9 @@ mod tests {
                 ],
                 totals: vec![
                     // The total is the change to _our_ balance spent during the transaction
-                    (token_id, (7_500 + fee) as i64),
+                    (token_id, TotalKind::Ours, (7_500 + fee) as i64),
+                    // And the SCI input - partial value returned
+                    (TokenId::from(2), TotalKind::Sci, (150) as i64),
                 ],
             },
         ];
@@ -598,8 +600,7 @@ mod tests {
                     associated_to_input_rules: target == &OutputTarget::Swap,
                 };
 
-                // Set address for normal outputs or SCIs
-                // TODO: do we _never_ have the output address for an SCI?
+                // Set address for normal outputs, not provided for SCIs
                 let address = match target != &OutputTarget::Swap {
                     true => Some((
                         ShortAddressHash::from(receive_subaddress),
@@ -674,7 +675,7 @@ mod tests {
             report.finalize().unwrap();
 
             // Check report totals
-            let totals: Vec<_> = report.totals.iter().map(|(t, v)| (*t, *v)).collect();
+            let totals: Vec<_> = report.totals.iter().map(|(t, k, v)| (*t, *k, *v)).collect();
             assert_eq!(&totals, &t.totals, "Total mismatch");
 
             // Check report outputs

From 486ae1282b46469b1928d88fd1e94178fb838137 Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Sat, 15 Jul 2023 11:08:09 +1200
Subject: [PATCH 03/11] rebase and update lock

---
 Cargo.lock                        | 87 +++----------------------------
 transaction/summary/src/report.rs |  2 +-
 2 files changed, 8 insertions(+), 81 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 1bdb82d072..c0de86af8c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1165,9 +1165,9 @@ dependencies = [
 
 [[package]]
 name = "dirs"
-version = "5.0.1"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
 dependencies = [
  "dirs-sys",
 ]
@@ -1184,14 +1184,13 @@ dependencies = [
 
 [[package]]
 name = "dirs-sys"
-version = "0.4.1"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
 dependencies = [
  "libc",
- "option-ext",
  "redox_users",
- "windows-sys 0.48.0",
+ "winapi",
 ]
 
 [[package]]
@@ -6249,12 +6248,6 @@ dependencies = [
  "thiserror",
 ]
 
-[[package]]
-name = "option-ext"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-
 [[package]]
 name = "ordered-float"
 version = "1.1.1"
@@ -8678,51 +8671,21 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
 dependencies = [
- "windows_aarch64_gnullvm 0.42.0",
+ "windows_aarch64_gnullvm",
  "windows_aarch64_msvc 0.42.0",
  "windows_i686_gnu 0.42.0",
  "windows_i686_msvc 0.42.0",
  "windows_x86_64_gnu 0.42.0",
- "windows_x86_64_gnullvm 0.42.0",
+ "windows_x86_64_gnullvm",
  "windows_x86_64_msvc 0.42.0",
 ]
 
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.0",
- "windows_aarch64_msvc 0.48.0",
- "windows_i686_gnu 0.48.0",
- "windows_i686_msvc 0.48.0",
- "windows_x86_64_gnu 0.48.0",
- "windows_x86_64_gnullvm 0.48.0",
- "windows_x86_64_msvc 0.48.0",
-]
-
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
 
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
-
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.36.1"
@@ -8735,12 +8698,6 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
 
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
-
 [[package]]
 name = "windows_i686_gnu"
 version = "0.36.1"
@@ -8753,12 +8710,6 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
 
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
-
 [[package]]
 name = "windows_i686_msvc"
 version = "0.36.1"
@@ -8771,12 +8722,6 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
 
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
-
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.36.1"
@@ -8789,24 +8734,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
 
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
-
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
 
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
-
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.36.1"
@@ -8819,12 +8752,6 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
 
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
-
 [[package]]
 name = "winnow"
 version = "0.4.0"
diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index 926dbda554..2f9e5a106f 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -43,7 +43,7 @@ pub trait TransactionReport {
     fn change_sub(&mut self, amount: Amount) -> Result<(), Error>;
 
     /// Add SCI input not owned by our account
-    fn sci_add(&mut self, _amount: Amount) -> Result<(), Error>;
+    fn sci_add(&mut self, amount: Amount) -> Result<(), Error>;
 
     /// Add output value for a particular entity / address to the report
     fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error>;

From 3ab66d3d296d617902fba06e4b408090401e629d Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Wed, 26 Jul 2023 11:23:10 +1200
Subject: [PATCH 04/11] add swap elision to report, update docs

---
 Cargo.lock                          | 135 +++++++++++++---------------
 transaction/summary/src/error.rs    |   2 +
 transaction/summary/src/report.rs   | 105 +++++++++++++++++++---
 transaction/summary/src/verifier.rs |   5 +-
 4 files changed, 162 insertions(+), 85 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c0de86af8c..d1e7c89c47 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -680,38 +680,30 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.1.14"
+version = "4.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "906f7fe1da4185b7a282b2bc90172a496f9def1aca4545fe7526810741591e14"
+checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
 dependencies = [
- "clap_builder",
+ "bitflags 2.0.2",
  "clap_derive",
- "once_cell",
-]
-
-[[package]]
-name = "clap_builder"
-version = "4.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "351f9ad9688141ed83dfd8f5fb998a06225ef444b48ff4dc43de6d409b7fd10b"
-dependencies = [
- "bitflags 1.3.2",
- "clap_lex 0.4.1",
+ "clap_lex 0.3.0",
  "is-terminal",
+ "once_cell",
  "strsim 0.10.0",
  "termcolor",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.1.14"
+version = "4.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81d7dc0031c3a59a04fc2ba395c8e2dd463cba1859275f065d225f6122221b45"
+checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
 dependencies = [
  "heck",
+ "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.12",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -725,9 +717,12 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.4.1"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
 
 [[package]]
 name = "clear_on_drop"
@@ -1184,9 +1179,9 @@ dependencies = [
 
 [[package]]
 name = "dirs-sys"
-version = "0.3.7"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
 dependencies = [
  "libc",
  "redox_users",
@@ -1594,7 +1589,7 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 name = "go-grpc-gateway-testing"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "futures",
  "grpcio",
@@ -2301,7 +2296,7 @@ dependencies = [
 name = "mc-admin-http-gateway"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "mc-common",
  "mc-util-grpc",
@@ -2848,7 +2843,7 @@ dependencies = [
 name = "mc-consensus-mint-client"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "grpcio",
  "hex",
@@ -2920,7 +2915,7 @@ dependencies = [
 name = "mc-consensus-scp-play"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-common",
  "mc-consensus-scp",
  "mc-transaction-core",
@@ -2950,7 +2945,7 @@ version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
  "chrono",
- "clap 4.1.14",
+ "clap 4.1.11",
  "curve25519-dalek",
  "displaydoc",
  "fs_extra",
@@ -3014,7 +3009,7 @@ name = "mc-consensus-service-config"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "hex",
  "mc-attest-core",
@@ -3040,7 +3035,7 @@ dependencies = [
 name = "mc-consensus-tool"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "mc-common",
  "mc-connection",
@@ -3055,7 +3050,7 @@ name = "mc-core"
 version = "5.0.8"
 dependencies = [
  "anyhow",
- "clap 4.1.14",
+ "clap 4.1.11",
  "curve25519-dalek",
  "ed25519-dalek",
  "generic-array",
@@ -3346,7 +3341,7 @@ name = "mc-crypto-x509-test-vectors"
 version = "5.0.8"
 dependencies = [
  "cargo-emit",
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-crypto-keys",
  "mc-util-build-script",
  "pem",
@@ -3412,7 +3407,7 @@ dependencies = [
 name = "mc-fog-distribution"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "crossbeam-channel",
  "curve25519-dalek",
  "grpcio",
@@ -3470,7 +3465,7 @@ name = "mc-fog-ingest-client"
 version = "5.0.8"
 dependencies = [
  "assert_cmd",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "grpcio",
  "hex",
@@ -3625,7 +3620,7 @@ dependencies = [
 name = "mc-fog-ingest-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "dirs",
  "displaydoc",
  "futures",
@@ -3847,7 +3842,7 @@ name = "mc-fog-ledger-server"
 version = "5.0.8"
 dependencies = [
  "aes-gcm",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "futures",
  "grpcio",
@@ -3930,7 +3925,7 @@ dependencies = [
 name = "mc-fog-load-testing"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "mc-account-keys",
  "mc-blockchain-test-utils",
@@ -4002,7 +3997,7 @@ dependencies = [
 name = "mc-fog-overseer-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "grpcio",
  "lazy_static",
@@ -4085,7 +4080,7 @@ name = "mc-fog-report-cli"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "hex",
  "mc-account-keys",
@@ -4140,7 +4135,7 @@ dependencies = [
 name = "mc-fog-report-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "futures",
  "grpcio",
@@ -4208,7 +4203,7 @@ name = "mc-fog-sample-paykit"
 version = "5.0.8"
 dependencies = [
  "cargo-emit",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "futures",
  "grpcio",
@@ -4306,7 +4301,7 @@ name = "mc-fog-sql-recovery-db"
 version = "5.0.8"
 dependencies = [
  "chrono",
- "clap 4.1.14",
+ "clap 4.1.11",
  "diesel",
  "diesel-derive-enum",
  "diesel_migrations",
@@ -4339,7 +4334,7 @@ name = "mc-fog-sql-recovery-db-cleanup"
 version = "5.0.8"
 dependencies = [
  "chrono",
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-common",
  "mc-fog-recovery-db-iface",
  "mc-fog-sql-recovery-db",
@@ -4350,7 +4345,7 @@ dependencies = [
 name = "mc-fog-test-client"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "grpcio",
  "hex_fmt",
@@ -4384,7 +4379,7 @@ dependencies = [
 name = "mc-fog-test-infra"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "digest 0.10.6",
  "hex",
  "mc-account-keys",
@@ -4577,7 +4572,7 @@ dependencies = [
 name = "mc-fog-view-load-test"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "mc-account-keys",
  "mc-attest-verifier",
@@ -4619,7 +4614,7 @@ dependencies = [
 name = "mc-fog-view-server"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "futures",
  "grpcio",
@@ -4730,7 +4725,7 @@ dependencies = [
 name = "mc-ledger-distribution"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "dirs",
  "displaydoc",
  "mc-api",
@@ -4753,7 +4748,7 @@ dependencies = [
 name = "mc-ledger-from-archive"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-api",
  "mc-common",
  "mc-ledger-db",
@@ -4764,7 +4759,7 @@ dependencies = [
 name = "mc-ledger-migration"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "lmdb-rkv",
  "mc-common",
  "mc-ledger-db",
@@ -4812,7 +4807,7 @@ name = "mc-mobilecoind"
 version = "5.0.8"
 dependencies = [
  "aes-gcm",
- "clap 4.1.14",
+ "clap 4.1.11",
  "crossbeam-channel",
  "displaydoc",
  "grpcio",
@@ -4900,7 +4895,7 @@ name = "mc-mobilecoind-dev-faucet"
 version = "5.0.8"
 dependencies = [
  "async-channel",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "grpcio",
  "hex",
@@ -4931,7 +4926,7 @@ dependencies = [
 name = "mc-mobilecoind-json"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "hex",
  "mc-api",
@@ -5111,7 +5106,7 @@ dependencies = [
 name = "mc-sgx-css-dump"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "hex_fmt",
  "mc-sgx-css",
 ]
@@ -5397,7 +5392,7 @@ name = "mc-transaction-signer"
 version = "5.0.8"
 dependencies = [
  "anyhow",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "hex",
  "log",
@@ -5426,19 +5421,15 @@ name = "mc-transaction-summary"
 version = "5.0.8"
 dependencies = [
  "displaydoc",
- "heapless",
  "mc-account-keys",
  "mc-core",
  "mc-crypto-digestible",
  "mc-crypto-keys",
  "mc-crypto-ring-signature",
- "mc-transaction-core",
  "mc-transaction-types",
- "mc-util-from-random",
  "mc-util-vec-map",
  "mc-util-zip-exact",
  "prost",
- "rand",
  "serde",
  "subtle",
  "zeroize",
@@ -5467,7 +5458,7 @@ dependencies = [
 name = "mc-util-b58-decoder"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "hex",
  "mc-account-keys",
  "mc-api",
@@ -5531,7 +5522,7 @@ dependencies = [
 name = "mc-util-cli"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-util-build-info",
 ]
 
@@ -5539,7 +5530,7 @@ dependencies = [
 name = "mc-util-dump-ledger"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "mc-blockchain-types",
  "mc-common",
@@ -5574,7 +5565,7 @@ dependencies = [
 name = "mc-util-generate-sample-ledger"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-account-keys",
  "mc-blockchain-test-utils",
  "mc-common",
@@ -5594,7 +5585,7 @@ name = "mc-util-grpc"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.14",
+ "clap 4.1.11",
  "cookie 0.17.0",
  "displaydoc",
  "futures",
@@ -5627,7 +5618,7 @@ dependencies = [
 name = "mc-util-grpc-admin-tool"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "grpcio",
  "mc-common",
  "mc-util-grpc",
@@ -5638,7 +5629,7 @@ dependencies = [
 name = "mc-util-grpc-token-generator"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-common",
  "mc-util-grpc",
  "mc-util-parse",
@@ -5654,7 +5645,7 @@ name = "mc-util-keyfile"
 version = "5.0.8"
 dependencies = [
  "base64 0.21.0",
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "hex",
  "mc-account-keys",
@@ -5742,7 +5733,7 @@ dependencies = [
 name = "mc-util-seeded-ed25519-key-gen"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "mc-crypto-keys",
  "mc-util-from-random",
  "mc-util-parse",
@@ -5778,7 +5769,7 @@ dependencies = [
 name = "mc-util-test-helper"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "itertools",
  "lazy_static",
  "mc-account-keys",
@@ -5860,7 +5851,7 @@ dependencies = [
 name = "mc-watcher"
 version = "5.0.8"
 dependencies = [
- "clap 4.1.14",
+ "clap 4.1.11",
  "displaydoc",
  "futures",
  "grpcio",
@@ -7460,9 +7451,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.164"
+version = "1.0.159"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
+checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
 dependencies = [
  "serde_derive",
 ]
@@ -7488,9 +7479,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.164"
+version = "1.0.159"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
+checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/transaction/summary/src/error.rs b/transaction/summary/src/error.rs
index 120071e7cc..06ab2c55f7 100644
--- a/transaction/summary/src/error.rs
+++ b/transaction/summary/src/error.rs
@@ -38,6 +38,8 @@ pub enum Error {
     Amount(AmountError),
     /// ZipExact error: {0}
     ZipExact(ZipExactError),
+    /// Missing address for own output
+    MissingOutputAddress,
 }
 
 impl From<BlockVersionError> for Error {
diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index 2f9e5a106f..66fc4d0e39 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -26,7 +26,7 @@ pub enum TransactionEntity {
     /// Outputs to other accounts (hash {0})
     OtherAddress(ShortAddressHash),
 
-    /// Swap counterparty
+    /// Outputs to swap counterparty
     Swap,
 }
 
@@ -117,10 +117,12 @@ pub struct TxSummaryUnblindingReport<
     /// Total balance change for our account for each type of token in the
     /// transaction.
     ///
-    /// totals = our inputs - sum(change outputs)
+    /// totals = inputs - sum(change outputs)
     ///
-    /// Note that swap inputs are elided as these are not inputs
-    /// owned by us (ie. are not spent from our account)
+    /// Note that owned and swap inputs are split as most
+    /// applications are concerned only with the _cost_ of
+    /// the transaction to the user.
+    /// See [elide_swap_totals] for more detail.
     pub totals: Vec<(TokenId, TotalKind, i64), TOTALS>,
 
     /// The network fee that we pay to execute the transaction
@@ -132,9 +134,10 @@ pub struct TxSummaryUnblindingReport<
 
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
 pub enum TotalKind {
-    /// Input owned by our account
+    /// Input owned by our account (less change), outgoing from our account
     Ours,
-    /// Input owned by SCI counterparty
+    /// Input owned by SCI counterparty (less change for partial swaps) incoming
+    /// to our account
     Sci,
 }
 
@@ -323,11 +326,32 @@ impl<const RECORDS: usize, const TOTALS: usize> TxSummaryUnblindingReport<RECORD
     pub fn sort(&mut self) {
         // TODO: should we remove zeroed balances / totals?
 
-        self.outputs[..].sort_by_key(|(_, t, _)| *t);
-        self.outputs[..].sort_by_key(|(e, _, _)| e.clone());
+        self.outputs[..].sort_by_key(|(e, t, _)| (e.clone(), *t));
+        self.totals[..].sort_by_key(|(t, k, _)| (*k, *t));
+    }
+
+    /// Elide SCI inputs and change outputs from a finalised report
+    ///
+    /// This transforms the report to only include outputs from _our_
+    /// account in the totals for display to users.
+    pub fn elide_swap_totals(&mut self) {
+        // Find SCI inputs in totals
+        for (token_id, kind, _) in &self.totals[..] {
+            if *kind != TotalKind::Sci {
+                continue;
+            }
+
+            // Remove corresponding change outputs for partial swap
+            // (change outputs are in the same token type as the swap,
+            // where the outputs to fulfil the swap are not)
+            self.outputs.retain(|(entity, token, _amount)| {
+                *entity != TransactionEntity::Swap || token != token_id
+            });
+        }
 
-        self.totals[..].sort_by_key(|(t, _, _)| *t);
-        self.totals[..].sort_by_key(|(_, k, _)| *k);
+        // Remove SCI inputs from totals
+        self.totals
+            .retain(|(_token_id, kind, _)| *kind != TotalKind::Sci)
     }
 }
 
@@ -443,4 +467,65 @@ mod tests {
             ]
         );
     }
+
+    #[test]
+    fn test_flatten_sci() {
+        let mut report = TxSummaryUnblindingReport::<16>::new();
+
+        let addr = TransactionEntity::OurAddress(ShortAddressHash::from(random::<[u8; 16]>()));
+
+        // Add SCI outputs
+        report
+            .output_add(TransactionEntity::Swap, Amount::new(10, TokenId::from(1)))
+            .unwrap();
+        report
+            .output_add(TransactionEntity::Swap, Amount::new(50, TokenId::from(2)))
+            .unwrap();
+
+        // Add output to us
+        report
+            .output_add(addr.clone(), Amount::new(50, TokenId::from(2)))
+            .unwrap();
+
+        // Add input from SCI
+        report.sci_add(Amount::new(100, TokenId::from(2))).unwrap();
+
+        // Add owned input
+        report.input_add(Amount::new(10, TokenId::from(1))).unwrap();
+
+        // Finalise report
+        report.finalize().unwrap();
+
+        // Check full outputs / totals
+        assert_eq!(
+            &report.outputs[..],
+            &[
+                (addr.clone(), TokenId::from(2), 50),
+                (TransactionEntity::Swap, TokenId::from(1), 10),
+                (TransactionEntity::Swap, TokenId::from(2), 50),
+            ]
+        );
+        assert_eq!(
+            &report.totals[..],
+            &[
+                (TokenId::from(1), TotalKind::Ours, 10),
+                (TokenId::from(2), TotalKind::Sci, 50),
+            ]
+        );
+
+        // Trim swap information (removes swap partial returns and totals)
+        report.elide_swap_totals();
+
+        assert_eq!(
+            &report.outputs[..],
+            &[
+                (addr.clone(), TokenId::from(2), 50),
+                (TransactionEntity::Swap, TokenId::from(1), 10),
+            ]
+        );
+        assert_eq!(
+            &report.totals[..],
+            &[(TokenId::from(1), TotalKind::Ours, 10),]
+        );
+    }
 }
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index 9eb40b2bcd..c9a5ddf96d 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -149,9 +149,8 @@ impl TxSummaryStreamingVerifierCtx {
                         .output_add(TransactionEntity::OurAddress(address_hash.clone()), amount)?;
                 }
             } else {
-                // TODO: If we _don't_ have address information but it's to our own address...
-                // what then? is this even possible??!
-                panic!("what's the right thing to do here..?");
+                // If we _don't_ have address information but it's to our own address...
+                return Err(Error::MissingOutputAddress);
             }
 
         // If we didn't match the output, and we have address information, this

From 735ef946e0ee022e05808f505870717d3ada9460 Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Wed, 20 Sep 2023 09:47:35 +1200
Subject: [PATCH 05/11] lockfile change

---
 Cargo.lock | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Cargo.lock b/Cargo.lock
index d1e7c89c47..dd1b6d4bb0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5421,15 +5421,19 @@ name = "mc-transaction-summary"
 version = "5.0.8"
 dependencies = [
  "displaydoc",
+ "heapless",
  "mc-account-keys",
  "mc-core",
  "mc-crypto-digestible",
  "mc-crypto-keys",
  "mc-crypto-ring-signature",
+ "mc-transaction-core",
  "mc-transaction-types",
+ "mc-util-from-random",
  "mc-util-vec-map",
  "mc-util-zip-exact",
  "prost",
+ "rand",
  "serde",
  "subtle",
  "zeroize",

From 3493a01f1a9d32128822335ce64e75210a909a5c Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Fri, 22 Sep 2023 10:56:53 +1200
Subject: [PATCH 06/11] fix report tests to include sci totals

---
 transaction/extra/tests/verifier.rs | 34 ++++++++++++++++++++++-------
 transaction/summary/src/lib.rs      |  2 +-
 transaction/summary/src/report.rs   |  2 ++
 3 files changed, 29 insertions(+), 9 deletions(-)

diff --git a/transaction/extra/tests/verifier.rs b/transaction/extra/tests/verifier.rs
index 439f6e382a..8b009f02c2 100644
--- a/transaction/extra/tests/verifier.rs
+++ b/transaction/extra/tests/verifier.rs
@@ -18,7 +18,7 @@ use mc_transaction_core::{
     Amount, BlockVersion, Token, TokenId,
 };
 use mc_transaction_extra::UnsignedTx;
-use mc_transaction_summary::{verify_tx_summary, TransactionEntity};
+use mc_transaction_summary::{verify_tx_summary, TotalKind, TransactionEntity};
 use mc_util_from_random::FromRandom;
 use mc_util_serial::encode;
 use rand::{rngs::StdRng, SeedableRng};
@@ -221,7 +221,10 @@ fn test_max_size_tx_summary_verification() {
             160
         )]
     );
-    assert_eq!(&report.totals, &[(TokenId::from(0), 16000),]);
+    assert_eq!(
+        &report.totals,
+        &[(TokenId::from(0), TotalKind::Ours, 16000),]
+    );
 
     assert_eq!(report.network_fee, Amount::new(15840, TokenId::from(0)));
 }
@@ -256,7 +259,10 @@ fn test_min_size_tx_summary_verification() {
             10
         )]
     );
-    assert_eq!(&report.totals, &[(TokenId::from(0), 1000),]);
+    assert_eq!(
+        &report.totals,
+        &[(TokenId::from(0), TotalKind::Ours, 1000),]
+    );
     assert_eq!(report.network_fee, Amount::new(990, TokenId::from(0)));
 }
 
@@ -343,7 +349,11 @@ fn test_two_input_tx_with_change_tx_summary_verification() {
         let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
         assert_eq!(
             &report.totals,
-            &[(token_id, (value + value2 - change_value) as i64),]
+            &[(
+                token_id,
+                TotalKind::Ours,
+                (value + value2 - change_value) as i64
+            ),]
         );
         assert_eq!(
             &report.outputs,
@@ -431,7 +441,7 @@ fn test_simple_tx_with_change_tx_summary_verification() {
         let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
         assert_eq!(
             &report.totals,
-            &[(token_id, ((value - change_value) as i64)),]
+            &[(token_id, TotalKind::Ours, ((value - change_value) as i64)),]
         );
         assert_eq!(
             &report.outputs,
@@ -523,7 +533,11 @@ fn test_two_output_tx_with_change_tx_summary_verification() {
         let recipient2_hash = ShortAddressHash::from(&recipient2.default_subaddress());
         assert_eq!(
             &report.totals,
-            &[(token_id, (value + value2 + Mob::MINIMUM_FEE) as i64),]
+            &[(
+                token_id,
+                TotalKind::Ours,
+                (value + value2 + Mob::MINIMUM_FEE) as i64
+            ),]
         );
         let mut outputs = vec![
             (
@@ -652,7 +666,9 @@ fn test_sci_tx_summary_verification() {
         &report.totals,
         &[
             // Bob spends 3x worth of token id 2 in the transaction
-            (token2, value2 as i64),
+            (token2, TotalKind::Ours, value2 as i64),
+            // SCI inputs used in the transaction
+            (Mob::ID, TotalKind::Sci, value as i64),
         ]
     );
     let mut outputs = vec![
@@ -781,7 +797,9 @@ fn test_sci_three_way_tx_summary_verification() {
         &report.totals,
         &[
             // Bob's spend to create the transaction
-            (token2, value2 as i64),
+            (token2, TotalKind::Ours, value2 as i64),
+            // SCI inputs used in the transaction
+            (Mob::ID, TotalKind::Sci, value as i64),
         ]
     );
     let mut outputs = vec![
diff --git a/transaction/summary/src/lib.rs b/transaction/summary/src/lib.rs
index f035f94f16..302631b14a 100644
--- a/transaction/summary/src/lib.rs
+++ b/transaction/summary/src/lib.rs
@@ -18,5 +18,5 @@ mod verifier;
 pub use data::{verify_tx_summary, TxOutSummaryUnblindingData, TxSummaryUnblindingData};
 
 pub use error::Error;
-pub use report::{TransactionEntity, TxSummaryUnblindingReport};
+pub use report::{TotalKind, TransactionEntity, TxSummaryUnblindingReport};
 pub use verifier::TxSummaryStreamingVerifierCtx;
diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index 66fc4d0e39..08aaa78f07 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -132,6 +132,8 @@ pub struct TxSummaryUnblindingReport<
     pub tombstone_block: u64,
 }
 
+/// Type of total balance, either `Ours` for inputs from our account or
+/// `Sci` for inputs from a swap counterparty.
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
 pub enum TotalKind {
     /// Input owned by our account (less change), outgoing from our account

From fdec87f4ee4ecb5a5a9ef23f6f5a4ea2f6da4d31 Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Fri, 22 Sep 2023 11:36:49 +1200
Subject: [PATCH 07/11] fix lints

---
 transaction/summary/src/report.rs   |  2 +-
 transaction/summary/src/verifier.rs | 32 ++++++++++++++---------------
 2 files changed, 16 insertions(+), 18 deletions(-)

diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index 08aaa78f07..a51df48d6d 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -521,7 +521,7 @@ mod tests {
         assert_eq!(
             &report.outputs[..],
             &[
-                (addr.clone(), TokenId::from(2), 50),
+                (addr, TokenId::from(2), 50),
                 (TransactionEntity::Swap, TokenId::from(1), 10),
             ]
         );
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index c9a5ddf96d..fdc48d14ee 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -394,7 +394,6 @@ mod tests {
     }
 
     #[derive(Clone, Debug, PartialEq)]
-    #[allow(dead_code)]
     enum InputType {
         /// An input we own, reducing our balance
         Owned,
@@ -403,7 +402,6 @@ mod tests {
     }
 
     #[derive(Clone, Debug, PartialEq)]
-    #[allow(dead_code)]
     enum OutputTarget {
         /// An output to ourself (_not_ a change address)
         Ourself,
@@ -439,7 +437,7 @@ mod tests {
             // Output to ourself, should show output to our address and total of output + fee
             TxOutReportTest {
                 inputs: vec![(InputType::Owned, Amount::new(amount.value + fee, token_id))],
-                outputs: vec![(OutputTarget::Ourself, amount.clone())],
+                outputs: vec![(OutputTarget::Ourself, amount)],
                 changes: vec![(
                     TransactionEntity::OurAddress(ShortAddressHash::from(&sender_subaddress)),
                     token_id,
@@ -456,7 +454,7 @@ mod tests {
                     ),
                     (InputType::Owned, Amount::new(amount.value / 2, token_id)),
                 ],
-                outputs: vec![(OutputTarget::Change, amount.clone())],
+                outputs: vec![(OutputTarget::Change, amount)],
                 changes: vec![
                     //(TransactionEntity::Total, token_id, 0),
                 ],
@@ -465,7 +463,7 @@ mod tests {
             // Output to someone else, should show their address and total of output + fee
             TxOutReportTest {
                 inputs: vec![(InputType::Owned, Amount::new(amount.value + fee, token_id))],
-                outputs: vec![(OutputTarget::Other, amount.clone())],
+                outputs: vec![(OutputTarget::Other, amount)],
                 changes: vec![(
                     TransactionEntity::OtherAddress(ShortAddressHash::from(&target_subaddress)),
                     token_id,
@@ -499,7 +497,7 @@ mod tests {
                     // The total is the change to _our_ balance spent during the transaction
                     (token_id, TotalKind::Ours, (10_000 + fee) as i64),
                     // And the SCI input
-                    (TokenId::from(2), TotalKind::Sci, (200) as i64),
+                    (TokenId::from(2), TotalKind::Sci, 200_i64),
                 ],
             },
             // Partial SCI
@@ -531,14 +529,14 @@ mod tests {
                     // The total is the change to _our_ balance spent during the transaction
                     (token_id, TotalKind::Ours, (7_500 + fee) as i64),
                     // And the SCI input - partial value returned
-                    (TokenId::from(2), TotalKind::Sci, (150) as i64),
+                    (TokenId::from(2), TotalKind::Sci, 150_i64),
                 ],
             },
         ];
 
         // Run tests
         for t in tests {
-            println!("Running test: {:?}", t);
+            println!("Running test: {t:?}");
 
             // Setup verifier
             let mut report = TxSummaryUnblindingReport::<16>::default();
@@ -547,16 +545,16 @@ mod tests {
                 BlockVersion::THREE,
                 t.outputs.len(),
                 t.inputs.len(),
-                sender.view_private_key().clone(),
+                *sender.view_private_key(),
                 PublicSubaddress {
-                    view_public: change_subaddress.view_public_key().clone().into(),
-                    spend_public: change_subaddress.spend_public_key().clone().into(),
+                    view_public: (*change_subaddress.view_public_key()).into(),
+                    spend_public: (*change_subaddress.spend_public_key()).into(),
                 },
             );
 
             // Build and process TxOuts
             for (target, amount) in &t.outputs {
-                println!("Add output {:?}: {:?}", target, amount);
+                println!("Add output {target:?}: {amount:?}");
 
                 // Select target address
                 let receive_subaddress = match target {
@@ -574,8 +572,8 @@ mod tests {
                 // Construct TxOut object
                 let tx_out = TxOut::new(
                     BlockVersion::THREE,
-                    amount.clone(),
-                    &receive_subaddress,
+                    *amount,
+                    receive_subaddress,
                     &tx_private_key,
                     Default::default(),
                 )
@@ -622,7 +620,7 @@ mod tests {
 
             // Build and process TxIns?
             for (kind, amount) in &t.inputs {
-                println!("Add input: {:?}", amount);
+                println!("Add input: {amount:?}");
 
                 // Setup keys for TxOut (kx against sender key as this is an input)
                 let tx_private_key = RistrettoPrivate::from_random(&mut rng);
@@ -632,7 +630,7 @@ mod tests {
                 // Construct TxOut object
                 let tx_out = TxOut::new(
                     BlockVersion::THREE,
-                    amount.clone(),
+                    *amount,
                     &sender_subaddress,
                     &tx_private_key,
                     Default::default(),
@@ -647,7 +645,7 @@ mod tests {
                     InputType::Sci => vec![0u8; 32],
                 };
                 let tx_in_summary = TxInSummary {
-                    pseudo_output_commitment: masked_amount.commitment().clone(),
+                    pseudo_output_commitment: *masked_amount.commitment(),
                     input_rules_digest,
                 };
 

From 6110326972f147ff6de436bdf6b5644a8c4cb152 Mon Sep 17 00:00:00 2001
From: ryan kurte <ryan@kurte.nz>
Date: Sat, 23 Sep 2023 12:40:45 +1200
Subject: [PATCH 08/11] swap transaction summary report to 128-bit types

---
 transaction/extra/tests/verifier.rs | 30 ++++++++--------
 transaction/summary/src/report.rs   | 53 +++++++++++++++++++----------
 transaction/summary/src/verifier.rs | 26 +++++++-------
 3 files changed, 63 insertions(+), 46 deletions(-)

diff --git a/transaction/extra/tests/verifier.rs b/transaction/extra/tests/verifier.rs
index 8b009f02c2..5ba8c449c9 100644
--- a/transaction/extra/tests/verifier.rs
+++ b/transaction/extra/tests/verifier.rs
@@ -352,7 +352,7 @@ fn test_two_input_tx_with_change_tx_summary_verification() {
             &[(
                 token_id,
                 TotalKind::Ours,
-                (value + value2 - change_value) as i64
+                (value + value2 - change_value) as i128
             ),]
         );
         assert_eq!(
@@ -360,7 +360,7 @@ fn test_two_input_tx_with_change_tx_summary_verification() {
             &[(
                 TransactionEntity::OtherAddress(recipient_hash),
                 token_id,
-                (value + value2 - change_value - Mob::MINIMUM_FEE)
+                (value + value2 - change_value - Mob::MINIMUM_FEE) as u128
             ),]
         );
         assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, token_id));
@@ -441,14 +441,14 @@ fn test_simple_tx_with_change_tx_summary_verification() {
         let recipient_hash = ShortAddressHash::from(&recipient.default_subaddress());
         assert_eq!(
             &report.totals,
-            &[(token_id, TotalKind::Ours, ((value - change_value) as i64)),]
+            &[(token_id, TotalKind::Ours, ((value - change_value) as i128)),]
         );
         assert_eq!(
             &report.outputs,
             &[(
                 TransactionEntity::OtherAddress(recipient_hash),
                 token_id,
-                (value - change_value - Mob::MINIMUM_FEE)
+                (value - change_value - Mob::MINIMUM_FEE) as u128
             ),]
         );
         assert_eq!(report.network_fee, Amount::new(Mob::MINIMUM_FEE, token_id));
@@ -536,19 +536,19 @@ fn test_two_output_tx_with_change_tx_summary_verification() {
             &[(
                 token_id,
                 TotalKind::Ours,
-                (value + value2 + Mob::MINIMUM_FEE) as i64
+                (value + value2 + Mob::MINIMUM_FEE) as i128
             ),]
         );
         let mut outputs = vec![
             (
                 TransactionEntity::OtherAddress(recipient_hash),
                 token_id,
-                value,
+                value as u128,
             ),
             (
                 TransactionEntity::OtherAddress(recipient2_hash),
                 token_id,
-                value2,
+                value2 as u128,
             ),
         ];
         outputs.sort();
@@ -666,19 +666,19 @@ fn test_sci_tx_summary_verification() {
         &report.totals,
         &[
             // Bob spends 3x worth of token id 2 in the transaction
-            (token2, TotalKind::Ours, value2 as i64),
+            (token2, TotalKind::Ours, value2 as i128),
             // SCI inputs used in the transaction
-            (Mob::ID, TotalKind::Sci, value as i64),
+            (Mob::ID, TotalKind::Sci, value as i128),
         ]
     );
     let mut outputs = vec![
         // Output to swap counterparty
-        (TransactionEntity::Swap, token2, value2),
+        (TransactionEntity::Swap, token2, value2 as u128),
         // Converted output to ourself
         (
             TransactionEntity::OurAddress(bob_hash),
             Mob::ID,
-            value - Mob::MINIMUM_FEE,
+            (value - Mob::MINIMUM_FEE) as u128,
         ),
     ];
     outputs.sort();
@@ -797,9 +797,9 @@ fn test_sci_three_way_tx_summary_verification() {
         &report.totals,
         &[
             // Bob's spend to create the transaction
-            (token2, TotalKind::Ours, value2 as i64),
+            (token2, TotalKind::Ours, value2 as i128),
             // SCI inputs used in the transaction
-            (Mob::ID, TotalKind::Sci, value as i64),
+            (Mob::ID, TotalKind::Sci, value as i128),
         ]
     );
     let mut outputs = vec![
@@ -807,10 +807,10 @@ fn test_sci_three_way_tx_summary_verification() {
         (
             TransactionEntity::OtherAddress(charlie_hash),
             Mob::ID,
-            (value - Mob::MINIMUM_FEE),
+            (value - Mob::MINIMUM_FEE) as u128,
         ),
         // Output to swap counterparty
-        (TransactionEntity::Swap, token2, value2),
+        (TransactionEntity::Swap, token2, value2 as u128),
     ];
     outputs.sort();
     assert_eq!(&report.outputs[..], &outputs[..]);
diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index a51df48d6d..fda1cc86fa 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -105,14 +105,15 @@ pub const MAX_TOTALS: usize = 4;
 /// balanced. For each token, totals = our inputs - sum(change outputs) ==
 /// sum(other outputs) + fee
 ///
-/// SCI inputs are currently ignored
+/// SCI inputs are also summed, and can be elided from the report with
+/// [TxSummaryUnblindingReport::elide_swap_totals]
 #[derive(Clone, Debug, Default)]
 pub struct TxSummaryUnblindingReport<
     const RECORDS: usize = MAX_RECORDS,
     const TOTALS: usize = MAX_TOTALS,
 > {
     /// Transaction outputs aggregated by address and token type
-    pub outputs: Vec<(TransactionEntity, TokenId, u64), RECORDS>,
+    pub outputs: Vec<(TransactionEntity, TokenId, u128), RECORDS>,
 
     /// Total balance change for our account for each type of token in the
     /// transaction.
@@ -120,10 +121,11 @@ pub struct TxSummaryUnblindingReport<
     /// totals = inputs - sum(change outputs)
     ///
     /// Note that owned and swap inputs are split as most
-    /// applications are concerned only with the _cost_ of
+    /// applications are concerned only with the cost of
     /// the transaction to the user.
+    ///
     /// See [elide_swap_totals] for more detail.
-    pub totals: Vec<(TokenId, TotalKind, i64), TOTALS>,
+    pub totals: Vec<(TokenId, TotalKind, i128), TOTALS>,
 
     /// The network fee that we pay to execute the transaction
     pub network_fee: Amount,
@@ -151,7 +153,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
         let Amount { token_id, value } = amount;
 
         // Ensure value will not overflow
-        let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
+        let value = i128::try_from(value).map_err(|_| Error::NumericOverflow)?;
 
         // Check for existing total entry for this token
         match self
@@ -176,7 +178,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
         let Amount { token_id, value } = amount;
 
         // Ensure value will not overflow
-        let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
+        let value = i128::try_from(value).map_err(|_| Error::NumericOverflow)?;
 
         // Check for existing total entry for this token
         match self
@@ -185,7 +187,11 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             .find(|(t, k, _)| t == &token_id && *k == TotalKind::Ours)
         {
             // If we have an entry, subtract the change value from this
-            Some(v) => v.2 = v.2.checked_sub(value).ok_or(Error::NumericOverflow)?,
+            Some(v) => {
+                v.2 =
+                    v.2.checked_sub(value as i128)
+                        .ok_or(Error::NumericOverflow)?
+            }
             // If we do not, create a new entry
             None => self
                 .totals
@@ -201,7 +207,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
         let Amount { token_id, value } = amount;
 
         // Ensure value will not overflow
-        let value = i64::try_from(value).map_err(|_| Error::NumericOverflow)?;
+        let value = i128::try_from(value).map_err(|_| Error::NumericOverflow)?;
 
         // Check for existing total entry for this token
         match self
@@ -210,7 +216,11 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             .find(|(t, k, _)| t == &token_id && *k == TotalKind::Sci)
         {
             // If we have an entry, add the value to this
-            Some(v) => v.2 = v.2.checked_add(value).ok_or(Error::NumericOverflow)?,
+            Some(v) => {
+                v.2 =
+                    v.2.checked_add(value as i128)
+                        .ok_or(Error::NumericOverflow)?
+            }
             // If we do not, create a new entry
             None => self
                 .totals
@@ -224,6 +234,9 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
     fn output_add(&mut self, entity: TransactionEntity, amount: Amount) -> Result<(), Error> {
         let Amount { token_id, value } = amount;
 
+        // Ensure value will not overflow
+        let value = u128::try_from(value).map_err(|_| Error::NumericOverflow)?;
+
         // Check for existing output for this address
         match self
             .outputs
@@ -263,11 +276,9 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
         self.sort();
 
         // For each token id, check that inputs match outputs
-        // (this is only executed where _totals_ exist, so skipped
-        // for the current SCI implementation)
         for (token_id, total_kind, value) in &mut self.totals {
             // Sum outputs for this token id
-            let mut balance = 0u64;
+            let mut balance = 0i128;
             for (e, id, v) in &self.outputs {
                 // Skip other tokens
                 if id != token_id {
@@ -279,16 +290,22 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
                 match total_kind {
                     // If it's coming from our account, track total balance
                     TotalKind::Ours => {
-                        balance = balance.checked_add(*v).ok_or(Error::NumericOverflow)?;
+                        balance = balance
+                            .checked_add(*v as i128)
+                            .ok_or(Error::NumericOverflow)?;
                     }
                     // If it's coming from an SCI, and returned to the counterparty, reduce total by
                     // outgoing value
                     TotalKind::Sci if e == &TransactionEntity::Swap => {
-                        *value = value.checked_sub(*v as i64).ok_or(Error::NumericOverflow)?;
+                        *value = value
+                            .checked_sub(*v as i128)
+                            .ok_or(Error::NumericOverflow)?;
                     }
                     // If it's coming from an SCI to us, add to total balance
                     TotalKind::Sci if e != &TransactionEntity::Swap => {
-                        balance = balance.checked_add(*v).ok_or(Error::NumericOverflow)?;
+                        balance = balance
+                            .checked_add(*v as i128)
+                            .ok_or(Error::NumericOverflow)?;
                     }
                     _ => (),
                 }
@@ -297,12 +314,12 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             // Add network fee for matching token id
             if &self.network_fee.token_id == token_id {
                 balance = balance
-                    .checked_add(self.network_fee.value)
+                    .checked_add(self.network_fee.value as i128)
                     .ok_or(Error::NumericOverflow)?;
             }
 
             // Check that the balance matches the total
-            if balance != *value as u64 {
+            if balance != *value {
                 return Err(Error::AmountVerificationFailed);
             }
         }
@@ -388,7 +405,7 @@ mod tests {
 
     #[test]
     fn test_report_size() {
-        assert_eq!(core::mem::size_of::<TxSummaryUnblindingReport>(), 1416);
+        assert_eq!(core::mem::size_of::<TxSummaryUnblindingReport>(), 1704);
     }
 
     #[test]
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index fdc48d14ee..8ea4996f53 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -388,9 +388,9 @@ mod tests {
         /// Outputs produced by the transaction
         outputs: Vec<(OutputTarget, Amount)>,
         /// Totals / balances by token
-        totals: Vec<(TokenId, TotalKind, i64)>,
+        totals: Vec<(TokenId, TotalKind, i128)>,
         /// Changes produced by the transaction
-        changes: Vec<(TransactionEntity, TokenId, u64)>,
+        changes: Vec<(TransactionEntity, TokenId, u128)>,
     }
 
     #[derive(Clone, Debug, PartialEq)]
@@ -441,9 +441,9 @@ mod tests {
                 changes: vec![(
                     TransactionEntity::OurAddress(ShortAddressHash::from(&sender_subaddress)),
                     token_id,
-                    amount.value,
+                    amount.value as u128,
                 )],
-                totals: vec![(token_id, TotalKind::Ours, (amount.value + fee) as i64)],
+                totals: vec![(token_id, TotalKind::Ours, (amount.value + fee) as i128)],
             },
             // Output to our change address, should show no outputs with balance change = fee
             TxOutReportTest {
@@ -458,7 +458,7 @@ mod tests {
                 changes: vec![
                     //(TransactionEntity::Total, token_id, 0),
                 ],
-                totals: vec![(token_id, TotalKind::Ours, fee as i64)],
+                totals: vec![(token_id, TotalKind::Ours, fee as i128)],
             },
             // Output to someone else, should show their address and total of output + fee
             TxOutReportTest {
@@ -467,9 +467,9 @@ mod tests {
                 changes: vec![(
                     TransactionEntity::OtherAddress(ShortAddressHash::from(&target_subaddress)),
                     token_id,
-                    amount.value,
+                    amount.value as u128,
                 )],
-                totals: vec![(token_id, TotalKind::Ours, (amount.value + fee) as i64)],
+                totals: vec![(token_id, TotalKind::Ours, (amount.value + fee) as i128)],
             },
             // Basic SCI. consuming entire swap, inputs should not count towards totals
             TxOutReportTest {
@@ -489,15 +489,15 @@ mod tests {
                     (
                         TransactionEntity::OurAddress(ShortAddressHash::from(&sender_subaddress)),
                         TokenId::from(2),
-                        200,
+                        200_u128,
                     ),
-                    (TransactionEntity::Swap, token_id, 10_000),
+                    (TransactionEntity::Swap, token_id, 10_000_u128),
                 ],
                 totals: vec![
                     // The total is the change to _our_ balance spent during the transaction
-                    (token_id, TotalKind::Ours, (10_000 + fee) as i64),
+                    (token_id, TotalKind::Ours, (10_000 + fee) as i128),
                     // And the SCI input
-                    (TokenId::from(2), TotalKind::Sci, 200_i64),
+                    (TokenId::from(2), TotalKind::Sci, 200_i128),
                 ],
             },
             // Partial SCI
@@ -527,9 +527,9 @@ mod tests {
                 ],
                 totals: vec![
                     // The total is the change to _our_ balance spent during the transaction
-                    (token_id, TotalKind::Ours, (7_500 + fee) as i64),
+                    (token_id, TotalKind::Ours, (7_500 + fee) as i128),
                     // And the SCI input - partial value returned
-                    (TokenId::from(2), TotalKind::Sci, 150_i64),
+                    (TokenId::from(2), TotalKind::Sci, 150_i128),
                 ],
             },
         ];

From 80cf635e8b5b6752bd17e97cd4a065d8f7faacb1 Mon Sep 17 00:00:00 2001
From: Henry Holtzman <henry@mobilecoin.com>
Date: Thu, 1 Aug 2024 15:51:36 -0700
Subject: [PATCH 09/11] lint fix; update Cargo.lock

---
 Cargo.lock                          | 46 +++++++++++++++++++++++++++--
 transaction/summary/src/verifier.rs |  5 +---
 2 files changed, 44 insertions(+), 7 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 6e36c12fd4..84e64868fa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -283,6 +283,15 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "atomic-polyfill"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
+dependencies = [
+ "critical-section",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -923,6 +932,12 @@ dependencies = [
  "itertools 0.10.5",
 ]
 
+[[package]]
+name = "critical-section"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
+
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.12"
@@ -1848,6 +1863,15 @@ version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
 
+[[package]]
+name = "hash32"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "hash32"
 version = "0.3.1"
@@ -1898,13 +1922,26 @@ dependencies = [
  "http",
 ]
 
+[[package]]
+name = "heapless"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
+dependencies = [
+ "atomic-polyfill",
+ "hash32 0.2.1",
+ "rustc_version",
+ "spin 0.9.6",
+ "stable_deref_trait",
+]
+
 [[package]]
 name = "heapless"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
 dependencies = [
- "hash32",
+ "hash32 0.3.1",
  "stable_deref_trait",
 ]
 
@@ -5968,7 +6005,7 @@ name = "mc-transaction-summary"
 version = "6.0.2"
 dependencies = [
  "displaydoc",
- "heapless",
+ "heapless 0.7.17",
  "mc-account-keys",
  "mc-core",
  "mc-crypto-digestible",
@@ -6374,7 +6411,7 @@ name = "mc-util-vec-map"
 version = "6.0.2"
 dependencies = [
  "displaydoc",
- "heapless",
+ "heapless 0.8.0",
 ]
 
 [[package]]
@@ -8542,6 +8579,9 @@ name = "spin"
 version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "spki"
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index 6d03d1abba..a0f2c5f41d 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -167,10 +167,7 @@ impl TxSummaryStreamingVerifierCtx {
                 Self::expected_tx_out_summary(self.block_version, amount, address, tx_private_key)?;
             if &expected == tx_out_summary {
                 // Add as an output to the report
-                report.output_add(
-                    TransactionEntity::OtherAddress(*address_hash),
-                    amount,
-                )?;
+                report.output_add(TransactionEntity::OtherAddress(*address_hash), amount)?;
             } else {
                 return Err(Error::AddressVerificationFailed);
             }

From 0cf5f7e232399b999ba20346e1154b165ae2f356 Mon Sep 17 00:00:00 2001
From: Henry Holtzman <henry@mobilecoin.com>
Date: Thu, 1 Aug 2024 16:15:37 -0700
Subject: [PATCH 10/11] lint fixes

---
 transaction/summary/src/report.rs   | 4 ++--
 transaction/summary/src/verifier.rs | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index fda1cc86fa..c590e8e4a6 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -189,7 +189,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             // If we have an entry, subtract the change value from this
             Some(v) => {
                 v.2 =
-                    v.2.checked_sub(value as i128)
+                    v.2.checked_sub(value)
                         .ok_or(Error::NumericOverflow)?
             }
             // If we do not, create a new entry
@@ -218,7 +218,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             // If we have an entry, add the value to this
             Some(v) => {
                 v.2 =
-                    v.2.checked_add(value as i128)
+                    v.2.checked_add(value)
                         .ok_or(Error::NumericOverflow)?
             }
             // If we do not, create a new entry
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index a0f2c5f41d..3bdbb69fcd 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -146,7 +146,7 @@ impl TxSummaryStreamingVerifierCtx {
                 } else {
                     // Otherwise, add this as an output to ourself
                     report
-                        .output_add(TransactionEntity::OurAddress(address_hash.clone()), amount)?;
+                        .output_add(TransactionEntity::OurAddress(*address_hash), amount)?;
                 }
             } else {
                 // If we _don't_ have address information but it's to our own address...

From bd88579060aa1125c0c95d38357d67792eb4e451 Mon Sep 17 00:00:00 2001
From: Henry Holtzman <henry@mobilecoin.com>
Date: Thu, 1 Aug 2024 16:33:45 -0700
Subject: [PATCH 11/11] lint fixes

---
 transaction/summary/src/report.rs   | 12 ++----------
 transaction/summary/src/verifier.rs |  3 +--
 2 files changed, 3 insertions(+), 12 deletions(-)

diff --git a/transaction/summary/src/report.rs b/transaction/summary/src/report.rs
index c590e8e4a6..61a21013b7 100644
--- a/transaction/summary/src/report.rs
+++ b/transaction/summary/src/report.rs
@@ -187,11 +187,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             .find(|(t, k, _)| t == &token_id && *k == TotalKind::Ours)
         {
             // If we have an entry, subtract the change value from this
-            Some(v) => {
-                v.2 =
-                    v.2.checked_sub(value)
-                        .ok_or(Error::NumericOverflow)?
-            }
+            Some(v) => v.2 = v.2.checked_sub(value).ok_or(Error::NumericOverflow)?,
             // If we do not, create a new entry
             None => self
                 .totals
@@ -216,11 +212,7 @@ impl<const RECORDS: usize, const TOTALS: usize> TransactionReport
             .find(|(t, k, _)| t == &token_id && *k == TotalKind::Sci)
         {
             // If we have an entry, add the value to this
-            Some(v) => {
-                v.2 =
-                    v.2.checked_add(value)
-                        .ok_or(Error::NumericOverflow)?
-            }
+            Some(v) => v.2 = v.2.checked_add(value).ok_or(Error::NumericOverflow)?,
             // If we do not, create a new entry
             None => self
                 .totals
diff --git a/transaction/summary/src/verifier.rs b/transaction/summary/src/verifier.rs
index 3bdbb69fcd..fc421384ad 100644
--- a/transaction/summary/src/verifier.rs
+++ b/transaction/summary/src/verifier.rs
@@ -145,8 +145,7 @@ impl TxSummaryStreamingVerifierCtx {
                     report.change_sub(amount)?;
                 } else {
                     // Otherwise, add this as an output to ourself
-                    report
-                        .output_add(TransactionEntity::OurAddress(*address_hash), amount)?;
+                    report.output_add(TransactionEntity::OurAddress(*address_hash), amount)?;
                 }
             } else {
                 // If we _don't_ have address information but it's to our own address...