mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Optimize decoy selection by batching the get_outputs call per input
This commit is contained in:
@@ -24,52 +24,61 @@ lazy_static! {
|
|||||||
static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
|
static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn select_single<R: RngCore + CryptoRng>(
|
async fn select_n<R: RngCore + CryptoRng>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
rpc: &Rpc,
|
rpc: &Rpc,
|
||||||
height: usize,
|
height: usize,
|
||||||
distribution: &[u64],
|
distribution: &[u64],
|
||||||
high: u64,
|
high: u64,
|
||||||
per_second: f64,
|
per_second: f64,
|
||||||
used: &mut HashSet<u64>
|
used: &mut HashSet<u64>,
|
||||||
) -> Result<(u64, [EdwardsPoint; 2]), RpcError> {
|
count: usize
|
||||||
|
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
|
||||||
// Panic if not enough decoys are available
|
// Panic if not enough decoys are available
|
||||||
// TODO: Simply create a TX with less than the target amount
|
// TODO: Simply create a TX with less than the target amount
|
||||||
if (high - MATURITY) < u64::try_from(DECOYS).unwrap() {
|
if (high - MATURITY) < u64::try_from(DECOYS).unwrap() {
|
||||||
panic!("Not enough decoys available");
|
panic!("Not enough decoys available");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut o;
|
let mut confirmed = Vec::with_capacity(count);
|
||||||
let mut output = None;
|
while confirmed.len() != count {
|
||||||
// Use a gamma distribution
|
let remaining = count - confirmed.len();
|
||||||
while {
|
let mut candidates = Vec::with_capacity(remaining);
|
||||||
let mut age = GAMMA.sample(rng).exp();
|
while candidates.len() != remaining {
|
||||||
if age > TIP_APPLICATION {
|
// Use a gamma distribution
|
||||||
age -= TIP_APPLICATION;
|
let mut age = GAMMA.sample(rng).exp();
|
||||||
} else {
|
if age > TIP_APPLICATION {
|
||||||
age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
|
age -= TIP_APPLICATION;
|
||||||
}
|
} else {
|
||||||
|
// f64 does not have try_from available, which is why these are written with `as`
|
||||||
|
age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
|
||||||
|
}
|
||||||
|
|
||||||
o = (age * per_second) as u64;
|
let o = (age * per_second) as u64;
|
||||||
(o >= high) || {
|
if o < high {
|
||||||
o = high - 1 - o;
|
let i = distribution.partition_point(|s| *s < (high - 1 - o));
|
||||||
let i = distribution.partition_point(|s| *s < o);
|
let prev = i.saturating_sub(1);
|
||||||
let prev = if i == 0 { 0 } else { i - 1 };
|
let n = distribution[i] - distribution[prev];
|
||||||
let n = distribution[i] - distribution[prev];
|
if n != 0 {
|
||||||
o = distribution[prev] + (rng.next_u64() % n.max(1));
|
let o = distribution[prev] + (rng.next_u64() % n);
|
||||||
(n == 0) || used.contains(&o) || {
|
if !used.contains(&o) {
|
||||||
output = rpc.get_outputs(&[o], height).await?[0];
|
// It will either actually be used, or is unusable and this prevents trying it again
|
||||||
if output.is_none() {
|
used.insert(o);
|
||||||
used.insert(o);
|
candidates.push(o);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
output.is_none()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} {}
|
|
||||||
|
|
||||||
// Insert the selected output
|
let outputs = rpc.get_outputs(&candidates, height).await?;
|
||||||
used.insert(o);
|
for i in 0 .. outputs.len() {
|
||||||
Ok((o, output.unwrap()))
|
if let Some(output) = outputs[i] {
|
||||||
|
confirmed.push((candidates[i], output));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses VarInt as this is solely used for key_offsets which is serialized by monero-rs
|
// Uses VarInt as this is solely used for key_offsets which is serialized by monero-rs
|
||||||
@@ -130,12 +139,9 @@ pub(crate) async fn select<R: RngCore + CryptoRng>(
|
|||||||
// TODO: If we're spending 2 outputs of a possible 11 outputs, this will still fail
|
// TODO: If we're spending 2 outputs of a possible 11 outputs, this will still fail
|
||||||
used.remove(&o.0);
|
used.remove(&o.0);
|
||||||
|
|
||||||
let mut decoys = Vec::with_capacity(DECOYS);
|
|
||||||
// Select the full amount of ring members in decoys, instead of just the actual decoys, in order
|
// Select the full amount of ring members in decoys, instead of just the actual decoys, in order
|
||||||
// to increase sample size
|
// to increase sample size
|
||||||
for _ in 0 .. DECOYS {
|
let mut decoys = select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS).await?;
|
||||||
decoys.push(select_single(rng, rpc, height, &distribution, high, per_second, &mut used).await?);
|
|
||||||
}
|
|
||||||
decoys.sort_by(|a, b| a.0.cmp(&b.0));
|
decoys.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
// Add back this output
|
// Add back this output
|
||||||
@@ -162,8 +168,12 @@ pub(crate) async fn select<R: RngCore + CryptoRng>(
|
|||||||
// it'd increase the amount of decoys required to create this transaction and some banned
|
// it'd increase the amount of decoys required to create this transaction and some banned
|
||||||
// outputs may be the best options
|
// outputs may be the best options
|
||||||
used.remove(&decoys[m].0);
|
used.remove(&decoys[m].0);
|
||||||
decoys[m] = select_single(rng, rpc, height, &distribution, high, per_second, &mut used).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decoys.splice(
|
||||||
|
0 .. DECOYS / 2,
|
||||||
|
select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS / 2).await?
|
||||||
|
);
|
||||||
decoys.sort_by(|a, b| a.0.cmp(&b.0));
|
decoys.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user