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:
Luke Parker 2022-06-19 12:03:01 -04:00
parent 71fca06120
commit f50f249468
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
11 changed files with 231 additions and 107 deletions

View file

@ -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)?;
}

View file

@ -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())?;

View file

@ -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)

View file

@ -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> {

View file

@ -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 {

View file

@ -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)

View file

@ -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! {

View file

@ -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;

View file

@ -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)?);

View file

@ -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(

View file

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