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"),