mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-10 21:19:24 +00:00
Bulletproofs+ (#70)
* Initial stab at Bulletproofs+ Does move around the existing Bulletproofs code, does still work as expected. * Make the Clsag RCTPrunable type work with BP and BP+ * Initial set of BP+ bug fixes * Further bug fixes * Remove RING_LEN as a constant * Monero v16 TX support Doesn't implement view tags, nor going back to v14, nor the updated BP clawback logic. * Support v14 and v16 at the same time
This commit is contained in:
@@ -8,7 +8,6 @@ use rand_distr::{Distribution, Gamma};
|
||||
use curve25519_dalek::edwards::EdwardsPoint;
|
||||
|
||||
use crate::{
|
||||
transaction::RING_LEN,
|
||||
wallet::SpendableOutput,
|
||||
rpc::{RpcError, Rpc},
|
||||
};
|
||||
@@ -20,8 +19,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 DECOYS: usize = RING_LEN - 1;
|
||||
|
||||
lazy_static! {
|
||||
static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
|
||||
static ref DISTRIBUTION: Mutex<Vec<u64>> = Mutex::new(Vec::with_capacity(3000000));
|
||||
@@ -109,9 +106,12 @@ impl Decoys {
|
||||
pub(crate) async fn select<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
ring_len: usize,
|
||||
height: usize,
|
||||
inputs: &[SpendableOutput],
|
||||
) -> Result<Vec<Decoys>, RpcError> {
|
||||
let decoy_count = ring_len - 1;
|
||||
|
||||
// Convert the inputs in question to the raw output data
|
||||
let mut outputs = Vec::with_capacity(inputs.len());
|
||||
for input in inputs {
|
||||
@@ -152,7 +152,7 @@ impl Decoys {
|
||||
}
|
||||
|
||||
// TODO: Simply create a TX with less than the target amount
|
||||
if (high - MATURITY) < u64::try_from(inputs.len() * RING_LEN).unwrap() {
|
||||
if (high - MATURITY) < u64::try_from(inputs.len() * ring_len).unwrap() {
|
||||
Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
|
||||
}
|
||||
|
||||
@@ -160,12 +160,12 @@ impl Decoys {
|
||||
// We should almost never naturally generate an insane transaction, hence why this doesn't
|
||||
// bother with an overage
|
||||
let mut decoys =
|
||||
select_n(rng, rpc, height, high, per_second, &mut used, inputs.len() * DECOYS).await?;
|
||||
select_n(rng, rpc, height, high, per_second, &mut used, inputs.len() * decoy_count).await?;
|
||||
|
||||
let mut res = Vec::with_capacity(inputs.len());
|
||||
for o in outputs {
|
||||
// Grab the decoys for this specific output
|
||||
let mut ring = decoys.drain((decoys.len() - DECOYS) ..).collect::<Vec<_>>();
|
||||
let mut ring = decoys.drain((decoys.len() - decoy_count) ..).collect::<Vec<_>>();
|
||||
ring.push(o);
|
||||
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
@@ -180,9 +180,9 @@ impl Decoys {
|
||||
if high > 500 {
|
||||
// Make sure the TX passes the sanity check that the median output is within the last 40%
|
||||
let target_median = high * 3 / 5;
|
||||
while ring[RING_LEN / 2].0 < target_median {
|
||||
while ring[ring_len / 2].0 < target_median {
|
||||
// If it's not, update the bottom half with new values to ensure the median only moves up
|
||||
for removed in ring.drain(0 .. (RING_LEN / 2)).collect::<Vec<_>>() {
|
||||
for removed in ring.drain(0 .. (ring_len / 2)).collect::<Vec<_>>() {
|
||||
// If we removed the real spend, add it back
|
||||
if removed.0 == o.0 {
|
||||
ring.push(o);
|
||||
@@ -197,7 +197,7 @@ impl Decoys {
|
||||
|
||||
// Select new outputs until we have a full sized ring again
|
||||
ring.extend(
|
||||
select_n(rng, rpc, height, high, per_second, &mut used, RING_LEN - ring.len()).await?,
|
||||
select_n(rng, rpc, height, high, per_second, &mut used, ring_len - ring.len()).await?,
|
||||
);
|
||||
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use monero::{consensus::Encodable, PublicKey, blockdata::transaction::SubField};
|
||||
use frost::FrostError;
|
||||
|
||||
use crate::{
|
||||
Commitment, random_scalar,
|
||||
Protocol, Commitment, random_scalar,
|
||||
ringct::{
|
||||
generate_key_image,
|
||||
clsag::{ClsagError, ClsagInput, Clsag},
|
||||
@@ -103,6 +103,7 @@ pub enum TransactionError {
|
||||
async fn prepare_inputs<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
ring_len: usize,
|
||||
inputs: &[SpendableOutput],
|
||||
spend: &Scalar,
|
||||
tx: &mut Transaction,
|
||||
@@ -113,6 +114,7 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
|
||||
let decoys = Decoys::select(
|
||||
rng,
|
||||
rpc,
|
||||
ring_len,
|
||||
rpc.get_height().await.map_err(TransactionError::RpcError)? - 10,
|
||||
inputs,
|
||||
)
|
||||
@@ -159,6 +161,7 @@ impl Fee {
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SignableTransaction {
|
||||
protocol: Protocol,
|
||||
inputs: Vec<SpendableOutput>,
|
||||
payments: Vec<(Address, u64)>,
|
||||
outputs: Vec<SendOutput>,
|
||||
@@ -167,6 +170,7 @@ pub struct SignableTransaction {
|
||||
|
||||
impl SignableTransaction {
|
||||
pub fn new(
|
||||
protocol: Protocol,
|
||||
inputs: Vec<SpendableOutput>,
|
||||
mut payments: Vec<(Address, u64)>,
|
||||
change_address: Option<Address>,
|
||||
@@ -200,14 +204,19 @@ impl SignableTransaction {
|
||||
if change && change_address.is_none() {
|
||||
Err(TransactionError::NoChange)?;
|
||||
}
|
||||
let mut outputs = payments.len() + (if change { 1 } else { 0 });
|
||||
let outputs = payments.len() + (if change { 1 } else { 0 });
|
||||
|
||||
// Calculate the extra length.
|
||||
// Type, length, value, with 1 field for the first key and 1 field for the rest
|
||||
let extra = (outputs * (2 + 32)) - (outputs.saturating_sub(2) * 2);
|
||||
|
||||
// Calculate the fee.
|
||||
let mut fee = fee_rate.calculate(Transaction::fee_weight(inputs.len(), outputs, extra));
|
||||
let mut fee = fee_rate.calculate(Transaction::fee_weight(
|
||||
protocol.ring_len(),
|
||||
inputs.len(),
|
||||
outputs,
|
||||
extra,
|
||||
));
|
||||
|
||||
// Make sure we have enough funds
|
||||
let in_amount = inputs.iter().map(|input| input.commitment.amount).sum::<u64>();
|
||||
@@ -219,25 +228,28 @@ impl SignableTransaction {
|
||||
// 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;
|
||||
let change_fee = fee_rate.calculate(Transaction::fee_weight(
|
||||
protocol.ring_len(),
|
||||
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)?;
|
||||
}
|
||||
|
||||
if change {
|
||||
payments.push((change_address.unwrap(), in_amount - out_amount));
|
||||
}
|
||||
|
||||
Ok(SignableTransaction { inputs, payments, outputs: vec![], fee })
|
||||
if payments.len() > MAX_OUTPUTS {
|
||||
Err(TransactionError::TooManyOutputs)?;
|
||||
}
|
||||
|
||||
Ok(SignableTransaction { protocol, inputs, payments, outputs: vec![], fee })
|
||||
}
|
||||
|
||||
fn prepare_outputs<R: RngCore + CryptoRng>(
|
||||
@@ -259,7 +271,14 @@ impl SignableTransaction {
|
||||
(commitments, sum)
|
||||
}
|
||||
|
||||
fn prepare_transaction(&self, commitments: &[Commitment], bp: Bulletproofs) -> Transaction {
|
||||
fn prepare_transaction<R: RngCore + CryptoRng>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
commitments: &[Commitment],
|
||||
) -> Transaction {
|
||||
// Safe due to the constructor checking MAX_OUTPUTS
|
||||
let bp = Bulletproofs::prove(rng, commitments, self.protocol.bp_plus()).unwrap();
|
||||
|
||||
// Create the TX extra
|
||||
// TODO: Review this for canonicity with Monero
|
||||
let mut extra = vec![];
|
||||
@@ -275,7 +294,11 @@ impl SignableTransaction {
|
||||
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() {
|
||||
tx_outputs.push(Output { amount: 0, key: self.outputs[o].dest, tag: None });
|
||||
tx_outputs.push(Output {
|
||||
amount: 0,
|
||||
key: self.outputs[o].dest,
|
||||
tag: Some(0).filter(|_| matches!(self.protocol, Protocol::v16)),
|
||||
});
|
||||
ecdh_info.push(self.outputs[o].amount);
|
||||
}
|
||||
|
||||
@@ -329,9 +352,10 @@ impl SignableTransaction {
|
||||
),
|
||||
);
|
||||
|
||||
let mut tx = self.prepare_transaction(&commitments, Bulletproofs::prove(rng, &commitments)?);
|
||||
let mut tx = self.prepare_transaction(rng, &commitments);
|
||||
|
||||
let signable = prepare_inputs(rng, rpc, &self.inputs, spend, &mut tx).await?;
|
||||
let signable =
|
||||
prepare_inputs(rng, rpc, self.protocol.ring_len(), &self.inputs, spend, &mut tx).await?;
|
||||
|
||||
let clsag_pairs = Clsag::sign(rng, &signable, mask_sum, tx.signature_hash());
|
||||
match tx.rct_signatures.prunable {
|
||||
|
||||
@@ -27,7 +27,6 @@ use crate::{
|
||||
random_scalar,
|
||||
ringct::{
|
||||
clsag::{ClsagInput, ClsagDetails, ClsagMultisig},
|
||||
bulletproofs::Bulletproofs,
|
||||
RctPrunable,
|
||||
},
|
||||
transaction::{Input, Transaction},
|
||||
@@ -143,6 +142,7 @@ impl SignableTransaction {
|
||||
// committed to. They'll also be committed to later via the TX message as a whole
|
||||
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys")),
|
||||
rpc,
|
||||
self.protocol.ring_len(),
|
||||
height,
|
||||
&self.inputs,
|
||||
)
|
||||
@@ -300,12 +300,8 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
);
|
||||
|
||||
self.signable.prepare_transaction(
|
||||
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"bulletproofs")),
|
||||
&commitments,
|
||||
Bulletproofs::prove(
|
||||
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"bulletproofs")),
|
||||
&commitments,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user