mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
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.
This commit is contained in:
@@ -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)?);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user