From f50f2494682fbb2e3dc0042a89d53286b41c8ccb Mon Sep 17 00:00:00 2001
From: Luke Parker <lukeparker5132@gmail.com>
Date: Sun, 19 Jun 2022 12:03:01 -0400
Subject: [PATCH] Add fee handling code to Monero

Updates how change outputs are handled, with a far more logical
construction offering greater flexibility.

prepare_outputs can not longer error. SignaableTransaction::new will.
---
 coins/monero/src/ringct/bulletproofs.rs  |  18 ++-
 coins/monero/src/ringct/clsag/mod.rs     |   7 +-
 coins/monero/src/ringct/mod.rs           |  12 ++
 coins/monero/src/rpc.rs                  |  59 +++++---
 coins/monero/src/serialize.rs            |   4 +
 coins/monero/src/transaction.rs          |  30 ++++
 coins/monero/src/wallet/decoys.rs        |   3 +-
 coins/monero/src/wallet/mod.rs           |   2 +-
 coins/monero/src/wallet/send/mod.rs      | 183 +++++++++++++++--------
 coins/monero/src/wallet/send/multisig.rs |  13 +-
 coins/monero/tests/send.rs               |   7 +-
 11 files changed, 231 insertions(+), 107 deletions(-)

diff --git a/coins/monero/src/ringct/bulletproofs.rs b/coins/monero/src/ringct/bulletproofs.rs
index 6a5866b2..e6a258d9 100644
--- a/coins/monero/src/ringct/bulletproofs.rs
+++ b/coins/monero/src/ringct/bulletproofs.rs
@@ -6,6 +6,8 @@ use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
 
 use crate::{Commitment, wallet::TransactionError, serialize::*};
 
+pub(crate) const MAX_OUTPUTS: usize = 16;
+
 #[derive(Clone, PartialEq, Debug)]
 pub struct Bulletproofs {
   pub A: EdwardsPoint,
@@ -22,8 +24,22 @@ pub struct Bulletproofs {
 }
 
 impl Bulletproofs {
+  pub(crate) fn fee_weight(outputs: usize) -> usize {
+    let proofs = 6 + usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap();
+    let len = (9 + (2 * proofs)) * 32;
+
+    let mut clawback = 0;
+    let padded = 1 << (proofs - 6);
+    if padded > 2 {
+      const BP_BASE: usize = 368;
+      clawback = ((BP_BASE * padded) - len) * 4 / 5;
+    }
+
+    len + clawback
+  }
+
   pub fn new<R: RngCore + CryptoRng>(rng: &mut R, outputs: &[Commitment]) -> Result<Bulletproofs, TransactionError> {
-    if outputs.len() > 16 {
+    if outputs.len() > MAX_OUTPUTS {
       return Err(TransactionError::TooManyOutputs)?;
     }
 
diff --git a/coins/monero/src/ringct/clsag/mod.rs b/coins/monero/src/ringct/clsag/mod.rs
index 215f08e4..80a50300 100644
--- a/coins/monero/src/ringct/clsag/mod.rs
+++ b/coins/monero/src/ringct/clsag/mod.rs
@@ -15,7 +15,8 @@ use crate::{
   Commitment,
   wallet::decoys::Decoys,
   random_scalar, hash_to_scalar, hash_to_point,
-  serialize::*
+  serialize::*,
+  transaction::RING_LEN
 };
 
 #[cfg(feature = "multisig")]
@@ -287,6 +288,10 @@ impl Clsag {
     Ok(())
   }
 
+  pub(crate) fn fee_weight() -> usize {
+    (RING_LEN * 32) + 32 + 32
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     write_raw_vec(write_scalar, &self.s, w)?;
     w.write_all(&self.c1.to_bytes())?;
diff --git a/coins/monero/src/ringct/mod.rs b/coins/monero/src/ringct/mod.rs
index 91b565b6..dbfc0fad 100644
--- a/coins/monero/src/ringct/mod.rs
+++ b/coins/monero/src/ringct/mod.rs
@@ -16,6 +16,10 @@ pub struct RctBase {
 }
 
 impl RctBase {
+  pub(crate) fn fee_weight(outputs: usize) -> usize {
+    1 + 8 + (outputs * (8 + 32))
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W, rct_type: u8) -> std::io::Result<()> {
     w.write_all(&[rct_type])?;
     match rct_type {
@@ -69,6 +73,10 @@ impl RctPrunable {
     }
   }
 
+  pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize {
+    1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight() + 32))
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     match self {
       RctPrunable::Null => Ok(()),
@@ -114,6 +122,10 @@ pub struct RctSignatures {
 }
 
 impl RctSignatures {
+  pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize {
+    RctBase::fee_weight(outputs) + RctPrunable::fee_weight(inputs, outputs)
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     self.base.serialize(w, self.prunable.rct_type())?;
     self.prunable.serialize(w)
diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs
index 7a3edf80..9a5ee8fe 100644
--- a/coins/monero/src/rpc.rs
+++ b/coins/monero/src/rpc.rs
@@ -9,7 +9,7 @@ use serde_json::json;
 
 use reqwest;
 
-use crate::{transaction::{Input, Timelock, Transaction}, block::Block};
+use crate::{transaction::{Input, Timelock, Transaction}, block::Block, wallet::Fee};
 
 #[derive(Deserialize, Debug)]
 pub struct EmptyResponse {}
@@ -34,9 +34,6 @@ pub enum RpcError {
   InvalidTransaction([u8; 32])
 }
 
-#[derive(Clone, Debug)]
-pub struct Rpc(String);
-
 fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
   hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string()))
 }
@@ -47,6 +44,9 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
   ).decompress().ok_or(RpcError::InvalidPoint(point.to_string()))
 }
 
+#[derive(Clone, Debug)]
+pub struct Rpc(String);
+
 impl Rpc {
   pub fn new(daemon: String) -> Rpc {
     Rpc(daemon)
@@ -233,6 +233,32 @@ impl Rpc {
     Ok(indexes.o_indexes)
   }
 
+  pub async fn get_output_distribution(&self, height: usize) -> Result<Vec<u64>, RpcError> {
+    #[allow(dead_code)]
+    #[derive(Deserialize, Debug)]
+    pub struct Distribution {
+      distribution: Vec<u64>
+    }
+
+    #[allow(dead_code)]
+    #[derive(Deserialize, Debug)]
+    struct Distributions {
+      distributions: Vec<Distribution>
+    }
+
+    let mut distributions: JsonRpcResponse<Distributions> = self.rpc_call("json_rpc", Some(json!({
+      "method": "get_output_distribution",
+      "params": {
+        "binary": false,
+        "amounts": [0],
+        "cumulative": true,
+        "to_height": height
+      }
+    }))).await?;
+
+    Ok(distributions.result.distributions.swap_remove(0).distribution)
+  }
+
   pub async fn get_outputs(
     &self,
     indexes: &[u64],
@@ -278,30 +304,19 @@ impl Rpc {
     ).collect()
   }
 
-  pub async fn get_output_distribution(&self, height: usize) -> Result<Vec<u64>, RpcError> {
+  pub async fn get_fee(&self) -> Result<Fee, RpcError> {
     #[allow(dead_code)]
     #[derive(Deserialize, Debug)]
-    pub struct Distribution {
-      distribution: Vec<u64>
+    struct FeeResponse {
+      fee: u64,
+      quantization_mask: u64
     }
 
-    #[allow(dead_code)]
-    #[derive(Deserialize, Debug)]
-    struct Distributions {
-      distributions: Vec<Distribution>
-    }
-
-    let mut distributions: JsonRpcResponse<Distributions> = self.rpc_call("json_rpc", Some(json!({
-      "method": "get_output_distribution",
-      "params": {
-        "binary": false,
-        "amounts": [0],
-        "cumulative": true,
-        "to_height": height
-      }
+    let res: JsonRpcResponse<FeeResponse> = self.rpc_call("json_rpc", Some(json!({
+      "method": "get_fee_estimate"
     }))).await?;
 
-    Ok(distributions.result.distributions.swap_remove(0).distribution)
+    Ok(Fee { per_weight: res.result.fee, mask: res.result.quantization_mask })
   }
 
   pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs
index 1303d43e..0ecd05b8 100644
--- a/coins/monero/src/serialize.rs
+++ b/coins/monero/src/serialize.rs
@@ -4,6 +4,10 @@ use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwards
 
 pub const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
 
+pub fn varint_len(varint: usize) -> usize {
+  ((usize::try_from(usize::BITS - varint.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1
+}
+
 pub fn write_varint<W: io::Write>(varint: &u64, w: &mut W) -> io::Result<()> {
   let mut varint = *varint;
   while {
diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs
index 338a16e5..32c68750 100644
--- a/coins/monero/src/transaction.rs
+++ b/coins/monero/src/transaction.rs
@@ -2,6 +2,8 @@ use curve25519_dalek::edwards::EdwardsPoint;
 
 use crate::{hash, serialize::*, ringct::{RctPrunable, RctSignatures}};
 
+pub const RING_LEN: usize = 11;
+
 #[derive(Clone, PartialEq, Debug)]
 pub enum Input {
   Gen(u64),
@@ -14,6 +16,13 @@ pub enum Input {
 }
 
 impl Input {
+  // Worst-case predictive len
+  pub(crate) fn fee_weight() -> usize {
+    // Uses 1 byte for the VarInt amount due to amount being 0
+    // Uses 1 byte for the VarInt encoding of the length of the ring as well
+    1 + 1 + 1 + (8 * RING_LEN) + 32
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     match self {
       Input::Gen(height) => {
@@ -56,6 +65,10 @@ pub struct Output {
 }
 
 impl Output {
+  pub(crate) fn fee_weight() -> usize {
+    1 + 1 + 32 + 1
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     write_varint(&self.amount, w)?;
     w.write_all(&[2 + (if self.tag.is_some() { 1 } else { 0 })])?;
@@ -102,6 +115,10 @@ impl Timelock {
     }
   }
 
+  pub(crate) fn fee_weight() -> usize {
+    8
+  }
+
   fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     write_varint(
       &match self {
@@ -124,6 +141,15 @@ pub struct TransactionPrefix {
 }
 
 impl TransactionPrefix {
+  pub(crate) fn fee_weight(inputs: usize, outputs: usize, extra: usize) -> usize {
+    // Assumes Timelock::None since this library won't let you create a TX with a timelock
+    1 + 1 +
+    varint_len(inputs) + (inputs * Input::fee_weight()) +
+    // Only 16 outputs are possible under transactions by this lib
+    1 + (outputs * Output::fee_weight()) +
+    varint_len(extra) + extra
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     write_varint(&self.version, w)?;
     self.timelock.serialize(w)?;
@@ -157,6 +183,10 @@ pub struct Transaction {
 }
 
 impl Transaction {
+  pub(crate) fn fee_weight(inputs: usize, outputs: usize, extra: usize) -> usize {
+    TransactionPrefix::fee_weight(inputs, outputs, extra) + RctSignatures::fee_weight(inputs, outputs)
+  }
+
   pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
     self.prefix.serialize(w)?;
     self.rct_signatures.serialize(w)
diff --git a/coins/monero/src/wallet/decoys.rs b/coins/monero/src/wallet/decoys.rs
index f205a735..405926bc 100644
--- a/coins/monero/src/wallet/decoys.rs
+++ b/coins/monero/src/wallet/decoys.rs
@@ -7,7 +7,7 @@ use rand_distr::{Distribution, Gamma};
 
 use curve25519_dalek::edwards::EdwardsPoint;
 
-use crate::{wallet::SpendableOutput, rpc::{RpcError, Rpc}};
+use crate::{transaction::RING_LEN, wallet::SpendableOutput, rpc::{RpcError, Rpc}};
 
 const LOCK_WINDOW: usize = 10;
 const MATURITY: u64 = 60;
@@ -16,7 +16,6 @@ const BLOCK_TIME: usize = 120;
 const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
 const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
 
-const RING_LEN: usize = 11;
 const DECOYS: usize = RING_LEN - 1;
 
 lazy_static! {
diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs
index e717fe4c..e0287eb4 100644
--- a/coins/monero/src/wallet/mod.rs
+++ b/coins/monero/src/wallet/mod.rs
@@ -13,7 +13,7 @@ pub(crate) mod decoys;
 pub(crate) use decoys::Decoys;
 
 mod send;
-pub use send::{TransactionError, SignableTransaction};
+pub use send::{Fee, TransactionError, SignableTransaction};
 #[cfg(feature = "multisig")]
 pub use send::TransactionMachine;
 
diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs
index 6a8c14dd..a9a689dd 100644
--- a/coins/monero/src/wallet/send/mod.rs
+++ b/coins/monero/src/wallet/send/mod.rs
@@ -24,7 +24,7 @@ use crate::{
   generate_key_image,
   ringct::{
     clsag::{ClsagError, ClsagInput, Clsag},
-    bulletproofs::Bulletproofs,
+    bulletproofs::{MAX_OUTPUTS, Bulletproofs},
     RctBase, RctPrunable, RctSignatures
   },
   transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
@@ -44,53 +44,53 @@ pub use multisig::TransactionMachine;
 struct SendOutput {
   R: EdwardsPoint,
   dest: EdwardsPoint,
-  mask: Scalar,
+  commitment: Commitment,
   amount: [u8; 8]
 }
 
 impl SendOutput {
   fn new<R: RngCore + CryptoRng>(
     rng: &mut R,
-    unique: Option<[u8; 32]>,
-    output: (Address, u64),
+    unique: [u8; 32],
+    output: (Address, u64, bool),
     o: usize
-  ) -> Result<SendOutput, TransactionError> {
+  ) -> SendOutput {
     let r = random_scalar(rng);
     let shared_key = shared_key(
-      unique,
+      Some(unique).filter(|_| output.2),
       r,
-      &output.0.public_view.point.decompress().ok_or(TransactionError::InvalidAddress)?,
+      &output.0.public_view.point.decompress().expect("SendOutput::new requires valid addresses"),
       o
     );
 
-    let spend = output.0.public_spend.point.decompress().ok_or(TransactionError::InvalidAddress)?;
-    Ok(
-      SendOutput {
-        R: match output.0.addr_type {
-          AddressType::Standard => Ok(&r * &ED25519_BASEPOINT_TABLE),
-          AddressType::SubAddress => Ok(&r * spend),
-          AddressType::Integrated(_) => Err(TransactionError::InvalidAddress)
-        }?,
-        dest: (&shared_key * &ED25519_BASEPOINT_TABLE) + spend,
-        mask: commitment_mask(shared_key),
-        amount: amount_encryption(output.1, shared_key)
-      }
-    )
+    let spend = output.0.public_spend.point.decompress().expect("SendOutput::new requires valid addresses");
+    SendOutput {
+      R: match output.0.addr_type {
+        AddressType::Standard => &r * &ED25519_BASEPOINT_TABLE,
+        AddressType::SubAddress => &r * spend,
+        AddressType::Integrated(_) => panic!("SendOutput::new doesn't support Integrated addresses")
+      },
+      dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + spend),
+      commitment: Commitment::new(commitment_mask(shared_key), output.1),
+      amount: amount_encryption(output.1, shared_key)
+    }
   }
 }
 
 #[derive(Clone, Error, Debug)]
 pub enum TransactionError {
+  #[error("invalid address")]
+  InvalidAddress,
   #[error("no inputs")]
   NoInputs,
   #[error("no outputs")]
   NoOutputs,
+  #[error("only one output and no change address")]
+  NoChange,
   #[error("too many outputs")]
   TooManyOutputs,
   #[error("not enough funds (in {0}, out {1})")]
   NotEnoughFunds(u64, u64),
-  #[error("invalid address")]
-  InvalidAddress,
   #[error("wrong spend private key")]
   WrongPrivateKey,
   #[error("rpc error ({0})")]
@@ -154,24 +154,56 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
   Ok(signable)
 }
 
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub struct Fee {
+  pub per_weight: u64,
+  pub mask: u64
+}
+
+impl Fee {
+  pub fn calculate(&self, weight: usize) -> u64 {
+    ((((self.per_weight * u64::try_from(weight).unwrap()) - 1) / self.mask) + 1) * self.mask
+  }
+}
+
 #[derive(Clone, PartialEq, Debug)]
 pub struct SignableTransaction {
   inputs: Vec<SpendableOutput>,
-  payments: Vec<(Address, u64)>,
-  change: Address,
-  fee_per_byte: u64,
-
-  fee: u64,
-  outputs: Vec<SendOutput>
+  payments: Vec<(Address, u64, bool)>,
+  outputs: Vec<SendOutput>,
+  fee: u64
 }
 
 impl SignableTransaction {
   pub fn new(
     inputs: Vec<SpendableOutput>,
     payments: Vec<(Address, u64)>,
-    change: Address,
-    fee_per_byte: u64
+    change_address: Option<Address>,
+    fee_rate: Fee
   ) -> Result<SignableTransaction, TransactionError> {
+    // Make sure all addresses are valid
+    let test = |addr: Address| {
+      if !(
+        addr.public_view.point.decompress().is_some() &&
+        addr.public_spend.point.decompress().is_some()
+      ) {
+        Err(TransactionError::InvalidAddress)?;
+      }
+
+      match addr.addr_type {
+        AddressType::Standard => Ok(()),
+        AddressType::Integrated(..) => Err(TransactionError::InvalidAddress),
+        AddressType::SubAddress => Ok(())
+      }
+    };
+
+    for payment in &payments {
+      test(payment.0)?;
+    }
+    if let Some(change) = change_address {
+      test(change)?;
+    }
+
     if inputs.len() == 0 {
       Err(TransactionError::NoInputs)?;
     }
@@ -179,15 +211,55 @@ impl SignableTransaction {
       Err(TransactionError::NoOutputs)?;
     }
 
+    // TODO TX MAX SIZE
+
+    // If we don't have two outputs, as required by Monero, add a second
+    let mut change = payments.len() == 1;
+    if change && change_address.is_none() {
+      Err(TransactionError::NoChange)?;
+    }
+    let mut outputs = payments.len() + (if change { 1 } else { 0 });
+
+    // Calculate the fee.
+    let extra = 0;
+    let mut fee = fee_rate.calculate(Transaction::fee_weight(inputs.len(), outputs, extra));
+
+    // Make sure we have enough funds
+    let in_amount = inputs.iter().map(|input| input.commitment.amount).sum::<u64>();
+    let mut out_amount = payments.iter().map(|payment| payment.1).sum::<u64>() + fee;
+    if in_amount < out_amount {
+      Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
+    }
+
+    // If we have yet to add a change output, do so if it's economically viable
+    if (!change) && change_address.is_some() && (in_amount != out_amount) {
+      // Check even with the new fee, there's remaining funds
+      let change_fee = fee_rate.calculate(Transaction::fee_weight(inputs.len(), outputs + 1, extra)) - fee;
+      if (out_amount + change_fee) < in_amount {
+        change = true;
+        outputs += 1;
+        out_amount += change_fee;
+        fee += change_fee;
+      }
+    }
+
+    if outputs > MAX_OUTPUTS {
+      Err(TransactionError::TooManyOutputs)?;
+    }
+
+    let mut payments = payments.iter().map(|(address, amount)| (*address, *amount, false)).collect::<Vec<_>>();
+    if change {
+      // Always use a unique key image for the change output
+      // TODO: Make this a config option
+      payments.push((change_address.unwrap(), in_amount - out_amount, true));
+    }
+
     Ok(
       SignableTransaction {
         inputs,
         payments,
-        change,
-        fee_per_byte,
-
-        fee: 0,
-        outputs: vec![]
+        outputs: vec![],
+        fee
       }
     )
   }
@@ -196,39 +268,19 @@ impl SignableTransaction {
     &mut self,
     rng: &mut R,
     uniqueness: [u8; 32]
-  ) -> Result<(Vec<Commitment>, Scalar), TransactionError> {
-    self.fee = self.fee_per_byte * 2000; // TODO
-
-    // TODO TX MAX SIZE
-
-    // Make sure we have enough funds
-    let in_amount = self.inputs.iter().map(|input| input.commitment.amount).sum();
-    let out_amount = self.fee + self.payments.iter().map(|payment| payment.1).sum::<u64>();
-    if in_amount < out_amount {
-      Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
-    }
-
-    let mut temp_outputs = Vec::with_capacity(self.payments.len() + 1);
-    // Add the payments to the outputs
-    for payment in &self.payments {
-      temp_outputs.push((None, (payment.0, payment.1)));
-    }
-    temp_outputs.push((Some(uniqueness), (self.change, in_amount - out_amount)));
-
-    // Shuffle the outputs
-    temp_outputs.shuffle(rng);
+  ) -> (Vec<Commitment>, Scalar) {
+    // Shuffle the payments
+    self.payments.shuffle(rng);
 
     // Actually create the outputs
-    self.outputs = Vec::with_capacity(temp_outputs.len());
-    let mut commitments = Vec::with_capacity(temp_outputs.len());
-    let mut mask_sum = Scalar::zero();
-    for (o, output) in temp_outputs.iter().enumerate() {
-      self.outputs.push(SendOutput::new(rng, output.0, output.1, o)?);
-      commitments.push(Commitment::new(self.outputs[o].mask, output.1.1));
-      mask_sum += self.outputs[o].mask;
+    self.outputs = Vec::with_capacity(self.payments.len() + 1);
+    for (o, output) in self.payments.iter().enumerate() {
+      self.outputs.push(SendOutput::new(rng, uniqueness, *output, o));
     }
 
-    Ok((commitments, mask_sum))
+    let commitments = self.outputs.iter().map(|output| output.commitment).collect::<Vec<_>>();
+    let sum = commitments.iter().map(|commitment| commitment.mask).sum();
+    (commitments, sum)
   }
 
   fn prepare_transaction(
@@ -246,7 +298,6 @@ impl SignableTransaction {
       self.outputs[1 ..].iter().map(|output| PublicKey { point: output.R.compress() }).collect()
     ).consensus_encode(&mut extra).unwrap();
 
-    // Format it for monero-rs
     let mut tx_outputs = Vec::with_capacity(self.outputs.len());
     let mut ecdh_info = Vec::with_capacity(self.outputs.len());
     for o in 0 .. self.outputs.len() {
@@ -307,7 +358,7 @@ impl SignableTransaction {
           key_image: *image
         }).collect::<Vec<_>>()
       )
-    )?;
+    );
 
     let mut tx = self.prepare_transaction(&commitments, Bulletproofs::new(rng, &commitments)?);
 
diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs
index 4b7d0f69..84dc63d7 100644
--- a/coins/monero/src/wallet/send/multisig.rs
+++ b/coins/monero/src/wallet/send/multisig.rs
@@ -35,9 +35,8 @@ pub struct TransactionMachine {
 }
 
 impl SignableTransaction {
-  pub async fn multisig<R: RngCore + CryptoRng>(
-    mut self,
-    rng: &mut R,
+  pub async fn multisig(
+    self,
     rpc: &Rpc,
     keys: MultisigKeys<Ed25519>,
     mut transcript: Transcript,
@@ -80,8 +79,8 @@ impl SignableTransaction {
     for payment in &self.payments {
       transcript.append_message(b"payment_address", &payment.0.as_bytes());
       transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
+      transcript.append_message(b"payment_unique", &(if payment.2 { [1] } else { [0] }));
     }
-    transcript.append_message(b"change", &self.change.as_bytes());
 
     // Sort included before cloning it around
     included.sort_unstable();
@@ -105,9 +104,6 @@ impl SignableTransaction {
       );
     }
 
-    // Verify these outputs by a dummy prep
-    self.prepare_outputs(rng, [0; 32])?;
-
     // Select decoys
     // Ideally, this would be done post entropy, instead of now, yet doing so would require sign
     // to be async which isn't preferable. This should be suitably competent though
@@ -228,7 +224,6 @@ impl StateMachine for TransactionMachine {
       let mut images = self.images.clone();
       images.sort_by(key_image_sort);
 
-      // Not invalid outputs due to already doing a dummy prep
       let (commitments, output_masks) = self.signable.prepare_outputs(
         &mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys")),
         uniqueness(
@@ -238,7 +233,7 @@ impl StateMachine for TransactionMachine {
             key_image: *image
           }).collect::<Vec<_>>()
         )
-      ).expect("Couldn't prepare outputs despite already doing a dummy prep");
+      );
       self.output_masks = Some(output_masks);
 
       self.signable.prepare_transaction(
diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs
index 4c002d45..9ef558dc 100644
--- a/coins/monero/tests/send.rs
+++ b/coins/monero/tests/send.rs
@@ -80,9 +80,7 @@ async fn send_core(test: usize, multisig: bool) {
     PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
   );
 
-  // TODO
-  let fee_per_byte = 50000000;
-  let fee = fee_per_byte * 2000;
+  let fee = rpc.get_fee().await.unwrap();
 
   let start = rpc.get_height().await.unwrap();
   for _ in 0 .. 7 {
@@ -134,7 +132,7 @@ async fn send_core(test: usize, multisig: bool) {
     }
 
     let mut signable = SignableTransaction::new(
-      outputs, vec![(addr, amount - fee)], addr, fee_per_byte
+      outputs, vec![(addr, amount - 10000000000)], Some(addr), fee
     ).unwrap();
 
     if !multisig {
@@ -147,7 +145,6 @@ async fn send_core(test: usize, multisig: bool) {
           machines.insert(
             i,
             signable.clone().multisig(
-              &mut OsRng,
               &rpc,
               (*keys[&i]).clone(),
               Transcript::new(b"Monero Serai Test Transaction"),