From 0f481773df523137a1bebe455d68a703c88bf379 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Wed, 4 May 2022 08:18:43 -0400 Subject: [PATCH] Use a gamma distribution for mixin selection --- coins/monero/Cargo.toml | 1 + coins/monero/src/rpc.rs | 32 ++++++++++---- coins/monero/src/transaction/mixins.rs | 53 +++++++++++++++++++----- coins/monero/src/transaction/mod.rs | 2 +- coins/monero/src/transaction/multisig.rs | 3 +- coins/monero/tests/send_multisig.rs | 1 + 6 files changed, 71 insertions(+), 21 deletions(-) diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index 390f34e1..75bf68ab 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -11,6 +11,7 @@ lazy_static = "1" thiserror = "1" rand_core = "0.6" +rand_distr = "0.4" tiny-keccak = { version = "2.0", features = ["keccak"] } blake2 = "0.10" diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs index 5be28804..a59cac50 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc.rs @@ -8,7 +8,6 @@ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; use monero::{ Hash, - cryptonote::hash::Hashable, blockdata::{ transaction::{TxIn, Transaction}, block::Block @@ -237,13 +236,30 @@ impl Rpc { ).collect() } - pub async fn get_high_output(&self, height: usize) -> Result { - let block = self.get_block(height).await?; - Ok( - *self.get_o_indexes( - *block.tx_hashes.last().unwrap_or(&block.miner_tx.hash()) - ).await?.last().ok_or(RpcError::InvalidTransaction)? - ) + pub async fn get_output_distribution(&self, height: usize) -> Result, RpcError> { + #[allow(dead_code)] + #[derive(Deserialize, Debug)] + pub struct Distribution { + distribution: Vec + } + + #[allow(dead_code)] + #[derive(Deserialize, Debug)] + struct Distributions { + distributions: Vec + } + + let mut distributions: JsonRpcResponse = 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 publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> { diff --git a/coins/monero/src/transaction/mixins.rs b/coins/monero/src/transaction/mixins.rs index d092d32e..6d283028 100644 --- a/coins/monero/src/transaction/mixins.rs +++ b/coins/monero/src/transaction/mixins.rs @@ -1,6 +1,9 @@ use std::collections::HashSet; +use lazy_static::lazy_static; + use rand_core::{RngCore, CryptoRng}; +use rand_distr::{Distribution, Gamma}; use curve25519_dalek::edwards::EdwardsPoint; @@ -8,22 +11,48 @@ use monero::VarInt; use crate::{transaction::SpendableOutput, rpc::{RpcError, Rpc}}; +const LOCK_WINDOW: usize = 10; +const RECENT_WINDOW: usize = 15; +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 MIXINS: usize = 11; +lazy_static! { + static ref GAMMA: Gamma = Gamma::new(19.28, 1.0 / 1.61).unwrap(); +} + async fn select_single( rng: &mut R, rpc: &Rpc, height: usize, + distribution: &[u64], high: u64, + per_second: f64, used: &mut HashSet ) -> Result<(u64, [EdwardsPoint; 2]), RpcError> { let mut o; let mut output = None; while { - o = rng.next_u64() % u64::try_from(high).unwrap(); - used.contains(&o) || { - output = rpc.get_outputs(&[o], height).await?[0]; - output.is_none() + let mut age = GAMMA.sample(rng).exp(); + if age > TIP_APPLICATION { + age -= TIP_APPLICATION; + } else { + age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64; + } + + o = (age * per_second) as u64; + (o >= high) || { + o = high - 1 - o; + let i = distribution.partition_point(|s| *s < o); + let prev = if i == 0 { 0 } else { i - 1 }; + let n = distribution[i] - distribution[prev]; + o = distribution[prev] + (rng.next_u64() % n); + (n == 0) || used.contains(&o) || { + output = rpc.get_outputs(&[o], height).await?[0]; + output.is_none() + } } } {} used.insert(o); @@ -55,11 +84,13 @@ pub(crate) async fn select( )); } - let high = rpc.get_high_output(height - 1).await?; - let high_f = high as f64; - if (high_f as u64) != high { - panic!("Transaction output index exceeds f64"); - } + let distribution = rpc.get_output_distribution(height).await?; + let high = distribution[distribution.len() - 1]; + let per_second = { + let blocks = distribution.len().min(BLOCKS_PER_YEAR); + let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)]; + (outputs as f64) / ((blocks * BLOCK_TIME) as f64) + }; let mut used = HashSet::::new(); for o in &outputs { @@ -70,7 +101,7 @@ pub(crate) async fn select( for (i, o) in outputs.iter().enumerate() { let mut mixins = Vec::with_capacity(MIXINS); for _ in 0 .. MIXINS { - mixins.push(select_single(rng, rpc, height, high, &mut used).await?); + mixins.push(select_single(rng, rpc, height, &distribution, high, per_second, &mut used).await?); } mixins.sort_by(|a, b| a.0.cmp(&b.0)); @@ -85,7 +116,7 @@ pub(crate) async fn select( // it'd increase the amount of mixins required to create this transaction and some banned // outputs may be the best options used.remove(&mixins[m].0); - mixins[m] = select_single(rng, rpc, height, high, &mut used).await?; + mixins[m] = select_single(rng, rpc, height, &distribution, high, per_second, &mut used).await?; } mixins.sort_by(|a, b| a.0.cmp(&b.0)); } diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs index 6fccb130..25a842d7 100644 --- a/coins/monero/src/transaction/mod.rs +++ b/coins/monero/src/transaction/mod.rs @@ -207,7 +207,7 @@ async fn prepare_inputs( let mixins = mixins::select( rng, rpc, - rpc.get_height().await.map_err(|e| TransactionError::RpcError(e))?, + rpc.get_height().await.map_err(|e| TransactionError::RpcError(e))? - 10, inputs ).await.map_err(|e| TransactionError::RpcError(e))?; diff --git a/coins/monero/src/transaction/multisig.rs b/coins/monero/src/transaction/multisig.rs index e4c0acbc..0aa379f8 100644 --- a/coins/monero/src/transaction/multisig.rs +++ b/coins/monero/src/transaction/multisig.rs @@ -38,6 +38,7 @@ impl SignableTransaction { rng: &mut R, rpc: &Rpc, keys: Rc>, + height: usize, included: &[usize] ) -> Result { let mut our_images = vec![]; @@ -75,7 +76,7 @@ impl SignableTransaction { let mixins = mixins::select( &mut transcript.seeded_rng(b"mixins", None), rpc, - rpc.get_height().await.map_err(|e| TransactionError::RpcError(e))?, + height, &self.inputs ).await.map_err(|e| TransactionError::RpcError(e))?; diff --git a/coins/monero/tests/send_multisig.rs b/coins/monero/tests/send_multisig.rs index b6df0c62..d60244b5 100644 --- a/coins/monero/tests/send_multisig.rs +++ b/coins/monero/tests/send_multisig.rs @@ -60,6 +60,7 @@ pub async fn send_multisig() { &mut OsRng, &rpc, keys[i - 1].clone(), + rpc.get_height().await.unwrap() - 10, &(1 ..= THRESHOLD).collect::>() ).await.unwrap() );