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:
Luke Parker 2022-07-27 04:05:43 -05:00 committed by GitHub
parent 37b8e3c025
commit 023afaf7ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 384 additions and 132 deletions

View file

@ -9,7 +9,7 @@ runs:
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: monerod path: monerod
key: monerod-${{ runner.os }}-${{ runner.arch }}-v0.17.3.2 key: monerod-${{ runner.os }}-${{ runner.arch }}-v0.18.0.0
- name: Download the Monero Daemon - name: Download the Monero Daemon
if: steps.cache-monerod.outputs.cache-hit != 'true' if: steps.cache-monerod.outputs.cache-hit != 'true'
@ -27,11 +27,11 @@ runs:
RUNNER_OS=linux RUNNER_OS=linux
RUNNER_ARCH=x64 RUNNER_ARCH=x64
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-v0.17.3.2.tar.bz2 FILE=monero-$RUNNER_OS-$RUNNER_ARCH-v0.18.0.0.tar.bz2
wget https://downloads.getmonero.org/cli/$FILE wget https://downloads.getmonero.org/cli/$FILE
tar -xvf $FILE tar -xvf $FILE
mv monero-x86_64-linux-gnu-v0.17.3.2/monerod monerod mv monero-x86_64-linux-gnu-v0.18.0.0/monerod monerod
- name: Monero Regtest Daemon - name: Monero Regtest Daemon
shell: bash shell: bash

View file

@ -25,6 +25,32 @@ pub mod wallet;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[allow(non_camel_case_types)]
pub enum Protocol {
Unsupported,
v14,
v16,
}
impl Protocol {
pub(crate) fn ring_len(&self) -> usize {
match self {
Protocol::Unsupported => panic!("Unsupported protocol version"),
Protocol::v14 => 11,
Protocol::v16 => 16,
}
}
pub(crate) fn bp_plus(&self) -> bool {
match self {
Protocol::Unsupported => panic!("Unsupported protocol version"),
Protocol::v14 => false,
Protocol::v16 => true,
}
}
}
lazy_static! { lazy_static! {
static ref H: EdwardsPoint = static ref H: EdwardsPoint =
CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes())) CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes()))

View file

@ -7,12 +7,16 @@ use rand_core::{RngCore, CryptoRng};
use curve25519_dalek::{scalar::Scalar as DalekScalar, edwards::EdwardsPoint as DalekPoint}; use curve25519_dalek::{scalar::Scalar as DalekScalar, edwards::EdwardsPoint as DalekPoint};
use group::{ff::Field, Group}; use group::{ff::Field, Group};
use dalek_ff_group::{Scalar, EdwardsPoint}; use dalek_ff_group::{ED25519_BASEPOINT_POINT, Scalar, EdwardsPoint};
use multiexp::multiexp; use multiexp::multiexp as const_multiexp;
fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
const_multiexp(pairs) * *INV_EIGHT
}
use crate::{ use crate::{
H as DALEK_H, Commitment, random_scalar as dalek_random, hash, hash_to_scalar as dalek_hash, H as DALEK_H, Commitment, hash, hash_to_scalar as dalek_hash,
ringct::{hash_to_point::raw_hash_to_point, bulletproofs::scalar_vector::*}, ringct::{hash_to_point::raw_hash_to_point, bulletproofs::scalar_vector::*},
serialize::write_varint, serialize::write_varint,
}; };
@ -23,10 +27,6 @@ lazy_static! {
static ref H: EdwardsPoint = EdwardsPoint(*DALEK_H); static ref H: EdwardsPoint = EdwardsPoint(*DALEK_H);
} }
fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
Scalar(dalek_random(rng))
}
fn hash_to_scalar(data: &[u8]) -> Scalar { fn hash_to_scalar(data: &[u8]) -> Scalar {
Scalar(dalek_hash(data)) Scalar(dalek_hash(data))
} }
@ -36,12 +36,6 @@ pub(crate) const MAX_M: usize = 16;
const N: usize = 64; const N: usize = 64;
const MAX_MN: usize = MAX_M * N; const MAX_MN: usize = MAX_M * N;
lazy_static! {
static ref ONE_N: ScalarVector = ScalarVector(vec![Scalar::one(); N]);
static ref TWO_N: ScalarVector = ScalarVector::powers(Scalar::from(2u8), N);
static ref IP12: Scalar = inner_product(&ONE_N, &TWO_N);
}
struct Generators { struct Generators {
G: Vec<EdwardsPoint>, G: Vec<EdwardsPoint>,
H: Vec<EdwardsPoint>, H: Vec<EdwardsPoint>,
@ -64,6 +58,7 @@ fn generators_core(prefix: &'static [u8]) -> Generators {
res res
} }
// TODO: Have this take in other, multiplied by G, and do a single multiexp
fn vector_exponent(generators: &Generators, a: &ScalarVector, b: &ScalarVector) -> EdwardsPoint { fn vector_exponent(generators: &Generators, a: &ScalarVector, b: &ScalarVector) -> EdwardsPoint {
debug_assert_eq!(a.len(), b.len()); debug_assert_eq!(a.len(), b.len());
(a * &generators.G[.. a.len()]) + (b * &generators.H[.. b.len()]) (a * &generators.G[.. a.len()]) + (b * &generators.H[.. b.len()])
@ -96,7 +91,7 @@ fn MN(outputs: usize) -> (usize, usize, usize) {
fn bit_decompose(commitments: &[Commitment]) -> (ScalarVector, ScalarVector) { fn bit_decompose(commitments: &[Commitment]) -> (ScalarVector, ScalarVector) {
let (_, M, MN) = MN(commitments.len()); let (_, M, MN) = MN(commitments.len());
let sv = ScalarVector(commitments.iter().cloned().map(|c| Scalar::from(c.amount)).collect()); let sv = commitments.iter().map(|c| Scalar::from(c.amount)).collect::<Vec<_>>();
let mut aL = ScalarVector::new(MN); let mut aL = ScalarVector::new(MN);
let mut aR = ScalarVector::new(MN); let mut aR = ScalarVector::new(MN);
@ -118,46 +113,63 @@ fn hash_commitments(commitments: &[Commitment]) -> Scalar {
hash_to_scalar(&V.iter().flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>()) hash_to_scalar(&V.iter().flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>())
} }
fn alpha<R: RngCore + CryptoRng>( fn alpha_rho<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
generators: &Generators, generators: &Generators,
aL: &ScalarVector, aL: &ScalarVector,
aR: &ScalarVector, aR: &ScalarVector,
) -> (Scalar, EdwardsPoint) { ) -> (Scalar, EdwardsPoint) {
let alpha = random_scalar(&mut *rng); let ar = Scalar::random(rng);
(alpha, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * alpha)) * *INV_EIGHT) (ar, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * ar)) * *INV_EIGHT)
}
fn LR_statements(
a: &ScalarVector,
G_i: &[EdwardsPoint],
b: &ScalarVector,
H_i: &[EdwardsPoint],
cL: Scalar,
U: EdwardsPoint,
) -> Vec<(Scalar, EdwardsPoint)> {
let mut res = a
.0
.iter()
.cloned()
.zip(G_i.iter().cloned())
.chain(b.0.iter().cloned().zip(H_i.iter().cloned()))
.collect::<Vec<_>>();
res.push((cL, U));
res
}
lazy_static! {
static ref TWO_N: ScalarVector = ScalarVector::powers(Scalar::from(2u8), N);
} }
// Bulletproofs-specific // Bulletproofs-specific
lazy_static! { lazy_static! {
static ref GENERATORS: Generators = generators_core(b"bulletproof"); static ref GENERATORS: Generators = generators_core(b"bulletproof");
static ref ONE_N: ScalarVector = ScalarVector(vec![Scalar::one(); N]);
static ref IP12: Scalar = inner_product(&ONE_N, &TWO_N);
} }
// Bulletproofs+-specific // Bulletproofs+-specific
lazy_static! { lazy_static! {
static ref GENERATORS_PLUS: Generators = generators_core(b"bulletproof_plus"); static ref GENERATORS_PLUS: Generators = generators_core(b"bulletproof_plus");
static ref TRANSCRIPT_PLUS: EdwardsPoint = static ref TRANSCRIPT_PLUS: [u8; 32] =
EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))); EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))).compress().to_bytes();
} }
fn even_powers_sum(x: Scalar, pow: usize) -> Scalar { // TRANSCRIPT_PLUS isn't a Scalar, so we need this alternative for the first hash
debug_assert!(pow != 0); fn hash_plus(mash: &[[u8; 32]]) -> Scalar {
// Verify pow is a power of two let slice =
debug_assert_eq!(((pow - 1) & pow), 0); &[&*TRANSCRIPT_PLUS as &[u8], mash.iter().cloned().flatten().collect::<Vec<_>>().as_ref()]
.concat();
let xsq = x * x; hash_to_scalar(slice)
let mut res = xsq;
let mut prev = 2;
while prev < pow {
res += res * xsq;
prev += 2;
}
res
} }
// Types for all Bulletproofs // Types for all Bulletproofs
#[allow(clippy::large_enum_variant)]
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Bulletproofs { pub enum Bulletproofs {
Original { Original {
@ -173,6 +185,17 @@ pub enum Bulletproofs {
b: DalekScalar, b: DalekScalar,
t: DalekScalar, t: DalekScalar,
}, },
Plus {
A: DalekPoint,
A1: DalekPoint,
B: DalekPoint,
r1: DalekScalar,
s1: DalekScalar,
d1: DalekScalar,
L: Vec<DalekPoint>,
R: Vec<DalekPoint>,
},
} }
pub(crate) fn prove<R: RngCore + CryptoRng>( pub(crate) fn prove<R: RngCore + CryptoRng>(
@ -183,12 +206,11 @@ pub(crate) fn prove<R: RngCore + CryptoRng>(
let (aL, aR) = bit_decompose(commitments); let (aL, aR) = bit_decompose(commitments);
let mut cache = hash_commitments(commitments); let mut cache = hash_commitments(commitments);
let (alpha, A) = alpha(rng, &GENERATORS, &aL, &aR); let (alpha, A) = alpha_rho(&mut *rng, &GENERATORS, &aL, &aR);
let (sL, sR) = let (sL, sR) =
ScalarVector((0 .. (MN * 2)).map(|_| random_scalar(&mut *rng)).collect::<Vec<_>>()).split(); ScalarVector((0 .. (MN * 2)).map(|_| Scalar::random(&mut *rng)).collect::<Vec<_>>()).split();
let rho = random_scalar(&mut *rng); let (rho, S) = alpha_rho(&mut *rng, &GENERATORS, &sL, &sR);
let S = (vector_exponent(&GENERATORS, &sL, &sR) + (EdwardsPoint::generator() * rho)) * *INV_EIGHT;
let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]); let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]);
let mut cache = hash_to_scalar(&y.to_bytes()); let mut cache = hash_to_scalar(&y.to_bytes());
@ -212,19 +234,18 @@ pub(crate) fn prove<R: RngCore + CryptoRng>(
let t1 = inner_product(&l0, &r1) + inner_product(&l1, &r0); let t1 = inner_product(&l0, &r1) + inner_product(&l1, &r0);
let t2 = inner_product(&l1, &r1); let t2 = inner_product(&l1, &r1);
let tau1 = random_scalar(&mut *rng); let tau1 = Scalar::random(&mut *rng);
let tau2 = random_scalar(&mut *rng); let tau2 = Scalar::random(rng);
let T1 = multiexp(&[(t1, *H), (tau1, EdwardsPoint::generator())]) * *INV_EIGHT; let T1 = prove_multiexp(&[(t1, *H), (tau1, EdwardsPoint::generator())]);
let T2 = multiexp(&[(t2, *H), (tau2, EdwardsPoint::generator())]) * *INV_EIGHT; let T2 = prove_multiexp(&[(t2, *H), (tau2, EdwardsPoint::generator())]);
let x = let x =
hash_cache(&mut cache, &[z.to_bytes(), T1.compress().to_bytes(), T2.compress().to_bytes()]); hash_cache(&mut cache, &[z.to_bytes(), T1.compress().to_bytes(), T2.compress().to_bytes()]);
let gamma = ScalarVector(commitments.iter().cloned().map(|c| Scalar(c.mask)).collect());
let mut taux = (tau2 * (x * x)) + (tau1 * x); let mut taux = (tau2 * (x * x)) + (tau1 * x);
for i in 1 ..= gamma.len() { for (i, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() {
taux += zpow[i + 1] * gamma[i - 1]; taux += zpow[i + 2] * gamma;
} }
let mu = (x * rho) + alpha; let mu = (x * rho) + alpha;
@ -259,26 +280,8 @@ pub(crate) fn prove<R: RngCore + CryptoRng>(
let (G_L, G_R) = G_proof.split_at(aL.len()); let (G_L, G_R) = G_proof.split_at(aL.len());
let (H_L, H_R) = H_proof.split_at(aL.len()); let (H_L, H_R) = H_proof.split_at(aL.len());
let mut L_i_s = aL let L_i = prove_multiexp(&LR_statements(&aL, G_R, &bR, H_L, cL, U));
.0 let R_i = prove_multiexp(&LR_statements(&aR, G_L, &bL, H_R, cR, U));
.iter()
.cloned()
.zip(G_R.iter().cloned())
.chain(bR.0.iter().cloned().zip(H_L.iter().cloned()))
.collect::<Vec<_>>();
L_i_s.push((cL, U));
let L_i = multiexp(&L_i_s) * *INV_EIGHT;
let mut R_i_s = aR
.0
.iter()
.cloned()
.zip(G_L.iter().cloned())
.chain(bL.0.iter().cloned().zip(H_R.iter().cloned()))
.collect::<Vec<_>>();
R_i_s.push((cR, U));
let R_i = multiexp(&R_i_s) * *INV_EIGHT;
L.push(L_i); L.push(L_i);
R.push(R_i); R.push(R_i);
@ -308,3 +311,113 @@ pub(crate) fn prove<R: RngCore + CryptoRng>(
t: *t, t: *t,
} }
} }
pub(crate) fn prove_plus<R: RngCore + CryptoRng>(
rng: &mut R,
commitments: &[Commitment],
) -> Bulletproofs {
let (logMN, M, MN) = MN(commitments.len());
let (aL, aR) = bit_decompose(commitments);
let mut cache = hash_plus(&[hash_commitments(commitments).to_bytes()]);
let (mut alpha1, A) = alpha_rho(&mut *rng, &GENERATORS_PLUS, &aL, &aR);
let y = hash_cache(&mut cache, &[A.compress().to_bytes()]);
let mut cache = hash_to_scalar(&y.to_bytes());
let z = cache;
let zpow = ScalarVector::even_powers(z, 2 * M);
// d[j*N+i] = z**(2*(j+1)) * 2**i
let mut d = vec![Scalar::zero(); MN];
for j in 0 .. M {
for i in 0 .. N {
d[(j * N) + i] = zpow[j] * TWO_N[i];
}
}
let aL1 = aL - z;
let ypow = ScalarVector::powers(y, MN + 2);
let mut y_for_d = ScalarVector(ypow.0[1 ..= MN].to_vec());
y_for_d.0.reverse();
let aR1 = (aR + z) + (y_for_d * ScalarVector(d));
for (j, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() {
alpha1 += zpow[j] * ypow[MN + 1] * gamma;
}
let mut a = aL1;
let mut b = aR1;
let yinv = y.invert().unwrap();
let yinvpow = ScalarVector::powers(yinv, MN);
let mut G_proof = GENERATORS_PLUS.G[.. a.len()].to_vec();
let mut H_proof = GENERATORS_PLUS.H[.. a.len()].to_vec();
let mut L = Vec::with_capacity(logMN);
let mut R = Vec::with_capacity(logMN);
while a.len() != 1 {
let (aL, aR) = a.split();
let (bL, bR) = b.split();
let cL = weighted_inner_product(&aL, &bR, y);
let cR = weighted_inner_product(&(&aR * ypow[aR.len()]), &bL, y);
let (dL, dR) = (Scalar::random(&mut *rng), Scalar::random(&mut *rng));
let (G_L, G_R) = G_proof.split_at(aL.len());
let (H_L, H_R) = H_proof.split_at(aL.len());
let mut L_i = LR_statements(&(&aL * yinvpow[aL.len()]), G_R, &bR, H_L, cL, *H);
L_i.push((dL, ED25519_BASEPOINT_POINT));
let L_i = prove_multiexp(&L_i);
L.push(L_i);
let mut R_i = LR_statements(&(&aR * ypow[aR.len()]), G_L, &bL, H_R, cR, *H);
R_i.push((dR, ED25519_BASEPOINT_POINT));
let R_i = prove_multiexp(&R_i);
R.push(R_i);
let w = hash_cache(&mut cache, &[L_i.compress().to_bytes(), R_i.compress().to_bytes()]);
let winv = w.invert().unwrap();
G_proof = hadamard_fold(G_L, G_R, winv, w * yinvpow[aL.len()]);
H_proof = hadamard_fold(H_L, H_R, w, winv);
a = (&aL * w) + (aR * (winv * ypow[aL.len()]));
b = (bL * winv) + (bR * w);
alpha1 += (dL * (w * w)) + (dR * (winv * winv));
}
let r = Scalar::random(&mut *rng);
let s = Scalar::random(&mut *rng);
let d = Scalar::random(&mut *rng);
let eta = Scalar::random(rng);
let A1 = prove_multiexp(&[
(r, G_proof[0]),
(s, H_proof[0]),
(d, ED25519_BASEPOINT_POINT),
((r * y * b[0]) + (s * y * a[0]), *H),
]);
let B = prove_multiexp(&[(r * y * s, *H), (eta, ED25519_BASEPOINT_POINT)]);
let e = hash_cache(&mut cache, &[A1.compress().to_bytes(), B.compress().to_bytes()]);
let r1 = (a[0] * e) + r;
let s1 = (b[0] * e) + s;
let d1 = ((d * e) + eta) + (alpha1 * (e * e));
Bulletproofs::Plus {
A: *A,
A1: *A1,
B: *B,
r1: *r1,
s1: *s1,
d1: *d1,
L: L.drain(..).map(|L| *L).collect(),
R: R.drain(..).map(|R| *R).collect(),
}
}

View file

@ -10,11 +10,12 @@ pub(crate) mod scalar_vector;
mod core; mod core;
pub(crate) use self::core::Bulletproofs; pub(crate) use self::core::Bulletproofs;
use self::core::{MAX_M, prove}; use self::core::{MAX_M, prove, prove_plus};
pub(crate) const MAX_OUTPUTS: usize = MAX_M; pub(crate) const MAX_OUTPUTS: usize = MAX_M;
impl Bulletproofs { impl Bulletproofs {
// TODO
pub(crate) fn fee_weight(outputs: usize) -> usize { pub(crate) fn fee_weight(outputs: usize) -> usize {
let proofs = 6 + usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap(); let proofs = 6 + usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap();
let len = (9 + (2 * proofs)) * 32; let len = (9 + (2 * proofs)) * 32;
@ -32,11 +33,12 @@ impl Bulletproofs {
pub fn prove<R: RngCore + CryptoRng>( pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
outputs: &[Commitment], outputs: &[Commitment],
plus: bool,
) -> Result<Bulletproofs, TransactionError> { ) -> Result<Bulletproofs, TransactionError> {
if outputs.len() > MAX_OUTPUTS { if outputs.len() > MAX_OUTPUTS {
return Err(TransactionError::TooManyOutputs)?; return Err(TransactionError::TooManyOutputs)?;
} }
Ok(prove(rng, outputs)) Ok(if !plus { prove(rng, outputs) } else { prove_plus(rng, outputs) })
} }
fn serialize_core<W: std::io::Write, F: Fn(&[EdwardsPoint], &mut W) -> std::io::Result<()>>( fn serialize_core<W: std::io::Write, F: Fn(&[EdwardsPoint], &mut W) -> std::io::Result<()>>(
@ -58,6 +60,17 @@ impl Bulletproofs {
write_scalar(b, w)?; write_scalar(b, w)?;
write_scalar(t, w) write_scalar(t, w)
} }
Bulletproofs::Plus { A, A1, B, r1, s1, d1, L, R } => {
write_point(A, w)?;
write_point(A1, w)?;
write_point(B, w)?;
write_scalar(r1, w)?;
write_scalar(s1, w)?;
write_scalar(d1, w)?;
specific_write_vec(L, w)?;
specific_write_vec(R, w)
}
} }
} }
@ -84,4 +97,17 @@ impl Bulletproofs {
t: read_scalar(r)?, t: read_scalar(r)?,
}) })
} }
pub fn deserialize_plus<R: std::io::Read>(r: &mut R) -> std::io::Result<Bulletproofs> {
Ok(Bulletproofs::Plus {
A: read_point(r)?,
A1: read_point(r)?,
B: read_point(r)?,
r1: read_scalar(r)?,
s1: read_scalar(r)?,
d1: read_scalar(r)?,
L: read_vec(read_point, r)?,
R: read_vec(read_point, r)?,
})
}
} }

View file

@ -60,6 +60,24 @@ impl ScalarVector {
ScalarVector(res) ScalarVector(res)
} }
pub(crate) fn even_powers(x: Scalar, pow: usize) -> ScalarVector {
debug_assert!(pow != 0);
// Verify pow is a power of two
debug_assert_eq!(((pow - 1) & pow), 0);
let xsq = x * x;
let mut res = ScalarVector(Vec::with_capacity(pow / 2));
res.0.push(xsq);
let mut prev = 2;
while prev < pow {
res.0.push(res[res.len() - 1] * xsq);
prev += 2;
}
res
}
pub(crate) fn sum(mut self) -> Scalar { pub(crate) fn sum(mut self) -> Scalar {
self.0.drain(..).sum() self.0.drain(..).sum()
} }
@ -86,7 +104,8 @@ pub(crate) fn inner_product(a: &ScalarVector, b: &ScalarVector) -> Scalar {
} }
pub(crate) fn weighted_inner_product(a: &ScalarVector, b: &ScalarVector, y: Scalar) -> Scalar { pub(crate) fn weighted_inner_product(a: &ScalarVector, b: &ScalarVector, y: Scalar) -> Scalar {
(a * b * ScalarVector::powers(y, a.len())).sum() // y ** 0 is not used as a power
(a * b * ScalarVector(ScalarVector::powers(y, a.len() + 1).0[1 ..].to_vec())).sum()
} }
impl Mul<&[EdwardsPoint]> for &ScalarVector { impl Mul<&[EdwardsPoint]> for &ScalarVector {

View file

@ -12,8 +12,8 @@ use curve25519_dalek::{
}; };
use crate::{ use crate::{
Commitment, random_scalar, hash_to_scalar, transaction::RING_LEN, wallet::decoys::Decoys, Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys, ringct::hash_to_point,
ringct::hash_to_point, serialize::*, serialize::*,
}; };
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
@ -292,8 +292,8 @@ impl Clsag {
Ok(()) Ok(())
} }
pub(crate) fn fee_weight() -> usize { pub(crate) fn fee_weight(ring_len: usize) -> usize {
(RING_LEN * 32) + 32 + 32 (ring_len * 32) + 32 + 32
} }
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> { pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {

View file

@ -31,7 +31,7 @@ impl RctBase {
w.write_all(&[rct_type])?; w.write_all(&[rct_type])?;
match rct_type { match rct_type {
0 => Ok(()), 0 => Ok(()),
5 => { 5 | 6 => {
write_varint(&self.fee, w)?; write_varint(&self.fee, w)?;
for ecdh in &self.ecdh_info { for ecdh in &self.ecdh_info {
w.write_all(ecdh)?; w.write_all(ecdh)?;
@ -78,18 +78,24 @@ impl RctPrunable {
pub fn rct_type(&self) -> u8 { pub fn rct_type(&self) -> u8 {
match self { match self {
RctPrunable::Null => 0, RctPrunable::Null => 0,
RctPrunable::Clsag { .. } => 5, RctPrunable::Clsag { bulletproofs, .. } => {
if matches!(bulletproofs[0], Bulletproofs::Original { .. }) {
5
} else {
6
}
}
} }
} }
pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize { pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize) -> usize {
1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight() + 32)) 1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight(ring_len) + 32))
} }
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> { pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
match self { match self {
RctPrunable::Null => Ok(()), RctPrunable::Null => Ok(()),
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs, .. } => {
write_vec(Bulletproofs::serialize, bulletproofs, w)?; write_vec(Bulletproofs::serialize, bulletproofs, w)?;
write_raw_vec(Clsag::serialize, clsags, w)?; write_raw_vec(Clsag::serialize, clsags, w)?;
write_raw_vec(write_point, pseudo_outs, w) write_raw_vec(write_point, pseudo_outs, w)
@ -104,8 +110,11 @@ impl RctPrunable {
) -> std::io::Result<RctPrunable> { ) -> std::io::Result<RctPrunable> {
Ok(match rct_type { Ok(match rct_type {
0 => RctPrunable::Null, 0 => RctPrunable::Null,
5 => RctPrunable::Clsag { 5 | 6 => RctPrunable::Clsag {
bulletproofs: read_vec(Bulletproofs::deserialize, r)?, bulletproofs: read_vec(
if rct_type == 5 { Bulletproofs::deserialize } else { Bulletproofs::deserialize_plus },
r,
)?,
clsags: (0 .. decoys.len()) clsags: (0 .. decoys.len())
.map(|o| Clsag::deserialize(decoys[o], r)) .map(|o| Clsag::deserialize(decoys[o], r))
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
@ -135,8 +144,8 @@ pub struct RctSignatures {
} }
impl RctSignatures { impl RctSignatures {
pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize { pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize) -> usize {
RctBase::fee_weight(outputs) + RctPrunable::fee_weight(inputs, outputs) RctBase::fee_weight(outputs) + RctPrunable::fee_weight(ring_len, inputs, outputs)
} }
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> { pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {

View file

@ -10,6 +10,7 @@ use serde_json::json;
use reqwest; use reqwest;
use crate::{ use crate::{
Protocol,
transaction::{Input, Timelock, Transaction}, transaction::{Input, Timelock, Transaction},
block::Block, block::Block,
wallet::Fee, wallet::Fee,
@ -98,6 +99,37 @@ impl Rpc {
}) })
} }
pub async fn get_protocol(&self) -> Result<Protocol, RpcError> {
#[derive(Deserialize, Debug)]
struct ProtocolResponse {
major_version: usize,
}
#[derive(Deserialize, Debug)]
struct LastHeaderResponse {
block_header: ProtocolResponse,
}
Ok(
match self
.rpc_call::<_, JsonRpcResponse<LastHeaderResponse>>(
"json_rpc",
Some(json!({
"method": "get_last_block_header"
})),
)
.await?
.result
.block_header
.major_version
{
13 | 14 => Protocol::v14,
15 | 16 => Protocol::v16,
_ => Protocol::Unsupported,
},
)
}
pub async fn get_height(&self) -> Result<usize, RpcError> { pub async fn get_height(&self) -> Result<usize, RpcError> {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct HeightResponse { struct HeightResponse {

View file

@ -8,8 +8,6 @@ use crate::{
ringct::{RctPrunable, RctSignatures}, ringct::{RctPrunable, RctSignatures},
}; };
pub const RING_LEN: usize = 11;
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Input { pub enum Input {
Gen(u64), Gen(u64),
@ -19,10 +17,10 @@ pub enum Input {
impl Input { impl Input {
// Worst-case predictive len // Worst-case predictive len
pub(crate) fn fee_weight() -> usize { pub(crate) fn fee_weight(ring_len: usize) -> usize {
// Uses 1 byte for the VarInt amount due to amount being 0 // 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 // Uses 1 byte for the VarInt encoding of the length of the ring as well
1 + 1 + 1 + (8 * RING_LEN) + 32 1 + 1 + 1 + (8 * ring_len) + 32
} }
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> { pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
@ -161,11 +159,11 @@ pub struct TransactionPrefix {
} }
impl TransactionPrefix { impl TransactionPrefix {
pub(crate) fn fee_weight(inputs: usize, outputs: usize, extra: usize) -> usize { pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize, extra: usize) -> usize {
// Assumes Timelock::None since this library won't let you create a TX with a timelock // Assumes Timelock::None since this library won't let you create a TX with a timelock
1 + 1 + 1 + 1 +
varint_len(inputs) + varint_len(inputs) +
(inputs * Input::fee_weight()) + (inputs * Input::fee_weight(ring_len)) +
1 + 1 +
(outputs * Output::fee_weight()) + (outputs * Output::fee_weight()) +
varint_len(extra) + varint_len(extra) +
@ -205,9 +203,9 @@ pub struct Transaction {
} }
impl Transaction { impl Transaction {
pub(crate) fn fee_weight(inputs: usize, outputs: usize, extra: usize) -> usize { pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize, extra: usize) -> usize {
TransactionPrefix::fee_weight(inputs, outputs, extra) + TransactionPrefix::fee_weight(ring_len, inputs, outputs, extra) +
RctSignatures::fee_weight(inputs, outputs) RctSignatures::fee_weight(ring_len, inputs, outputs)
} }
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> { pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {

View file

@ -8,7 +8,6 @@ use rand_distr::{Distribution, Gamma};
use curve25519_dalek::edwards::EdwardsPoint; use curve25519_dalek::edwards::EdwardsPoint;
use crate::{ use crate::{
transaction::RING_LEN,
wallet::SpendableOutput, wallet::SpendableOutput,
rpc::{RpcError, Rpc}, rpc::{RpcError, Rpc},
}; };
@ -20,8 +19,6 @@ const BLOCK_TIME: usize = 120;
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME; const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64; const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
const DECOYS: usize = RING_LEN - 1;
lazy_static! { 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();
static ref DISTRIBUTION: Mutex<Vec<u64>> = Mutex::new(Vec::with_capacity(3000000)); 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>( pub(crate) async fn select<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
rpc: &Rpc, rpc: &Rpc,
ring_len: usize,
height: usize, height: usize,
inputs: &[SpendableOutput], inputs: &[SpendableOutput],
) -> Result<Vec<Decoys>, RpcError> { ) -> Result<Vec<Decoys>, RpcError> {
let decoy_count = ring_len - 1;
// Convert the inputs in question to the raw output data // Convert the inputs in question to the raw output data
let mut outputs = Vec::with_capacity(inputs.len()); let mut outputs = Vec::with_capacity(inputs.len());
for input in inputs { for input in inputs {
@ -152,7 +152,7 @@ impl Decoys {
} }
// 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(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()))?; 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 // We should almost never naturally generate an insane transaction, hence why this doesn't
// bother with an overage // bother with an overage
let mut decoys = 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()); let mut res = Vec::with_capacity(inputs.len());
for o in outputs { for o in outputs {
// Grab the decoys for this specific output // 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.push(o);
ring.sort_by(|a, b| a.0.cmp(&b.0)); ring.sort_by(|a, b| a.0.cmp(&b.0));
@ -180,9 +180,9 @@ impl Decoys {
if high > 500 { if high > 500 {
// Make sure the TX passes the sanity check that the median output is within the last 40% // Make sure the TX passes the sanity check that the median output is within the last 40%
let target_median = high * 3 / 5; 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 // 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 we removed the real spend, add it back
if removed.0 == o.0 { if removed.0 == o.0 {
ring.push(o); ring.push(o);
@ -197,7 +197,7 @@ impl Decoys {
// Select new outputs until we have a full sized ring again // Select new outputs until we have a full sized ring again
ring.extend( 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)); ring.sort_by(|a, b| a.0.cmp(&b.0));
} }

View file

@ -11,7 +11,7 @@ use monero::{consensus::Encodable, PublicKey, blockdata::transaction::SubField};
use frost::FrostError; use frost::FrostError;
use crate::{ use crate::{
Commitment, random_scalar, Protocol, Commitment, random_scalar,
ringct::{ ringct::{
generate_key_image, generate_key_image,
clsag::{ClsagError, ClsagInput, Clsag}, clsag::{ClsagError, ClsagInput, Clsag},
@ -103,6 +103,7 @@ pub enum TransactionError {
async fn prepare_inputs<R: RngCore + CryptoRng>( async fn prepare_inputs<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
rpc: &Rpc, rpc: &Rpc,
ring_len: usize,
inputs: &[SpendableOutput], inputs: &[SpendableOutput],
spend: &Scalar, spend: &Scalar,
tx: &mut Transaction, tx: &mut Transaction,
@ -113,6 +114,7 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
let decoys = Decoys::select( let decoys = Decoys::select(
rng, rng,
rpc, rpc,
ring_len,
rpc.get_height().await.map_err(TransactionError::RpcError)? - 10, rpc.get_height().await.map_err(TransactionError::RpcError)? - 10,
inputs, inputs,
) )
@ -159,6 +161,7 @@ impl Fee {
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct SignableTransaction { pub struct SignableTransaction {
protocol: Protocol,
inputs: Vec<SpendableOutput>, inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>, payments: Vec<(Address, u64)>,
outputs: Vec<SendOutput>, outputs: Vec<SendOutput>,
@ -167,6 +170,7 @@ pub struct SignableTransaction {
impl SignableTransaction { impl SignableTransaction {
pub fn new( pub fn new(
protocol: Protocol,
inputs: Vec<SpendableOutput>, inputs: Vec<SpendableOutput>,
mut payments: Vec<(Address, u64)>, mut payments: Vec<(Address, u64)>,
change_address: Option<Address>, change_address: Option<Address>,
@ -200,14 +204,19 @@ impl SignableTransaction {
if change && change_address.is_none() { if change && change_address.is_none() {
Err(TransactionError::NoChange)?; 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. // Calculate the extra length.
// Type, length, value, with 1 field for the first key and 1 field for the rest // 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); let extra = (outputs * (2 + 32)) - (outputs.saturating_sub(2) * 2);
// Calculate the fee. // 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 // Make sure we have enough funds
let in_amount = inputs.iter().map(|input| input.commitment.amount).sum::<u64>(); 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 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) { if (!change) && change_address.is_some() && (in_amount != out_amount) {
// Check even with the new fee, there's remaining funds // Check even with the new fee, there's remaining funds
let change_fee = let change_fee = fee_rate.calculate(Transaction::fee_weight(
fee_rate.calculate(Transaction::fee_weight(inputs.len(), outputs + 1, extra)) - fee; protocol.ring_len(),
inputs.len(),
outputs + 1,
extra,
)) - fee;
if (out_amount + change_fee) < in_amount { if (out_amount + change_fee) < in_amount {
change = true; change = true;
outputs += 1;
out_amount += change_fee; out_amount += change_fee;
fee += change_fee; fee += change_fee;
} }
} }
if outputs > MAX_OUTPUTS {
Err(TransactionError::TooManyOutputs)?;
}
if change { if change {
payments.push((change_address.unwrap(), in_amount - out_amount)); 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>( fn prepare_outputs<R: RngCore + CryptoRng>(
@ -259,7 +271,14 @@ impl SignableTransaction {
(commitments, sum) (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 // Create the TX extra
// TODO: Review this for canonicity with Monero // TODO: Review this for canonicity with Monero
let mut extra = vec![]; let mut extra = vec![];
@ -275,7 +294,11 @@ impl SignableTransaction {
let mut tx_outputs = Vec::with_capacity(self.outputs.len()); let mut tx_outputs = Vec::with_capacity(self.outputs.len());
let mut ecdh_info = Vec::with_capacity(self.outputs.len()); let mut ecdh_info = Vec::with_capacity(self.outputs.len());
for o in 0 .. 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); 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()); let clsag_pairs = Clsag::sign(rng, &signable, mask_sum, tx.signature_hash());
match tx.rct_signatures.prunable { match tx.rct_signatures.prunable {

View file

@ -27,7 +27,6 @@ use crate::{
random_scalar, random_scalar,
ringct::{ ringct::{
clsag::{ClsagInput, ClsagDetails, ClsagMultisig}, clsag::{ClsagInput, ClsagDetails, ClsagMultisig},
bulletproofs::Bulletproofs,
RctPrunable, RctPrunable,
}, },
transaction::{Input, Transaction}, 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 // 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")), &mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys")),
rpc, rpc,
self.protocol.ring_len(),
height, height,
&self.inputs, &self.inputs,
) )
@ -300,12 +300,8 @@ impl SignMachine<Transaction> for TransactionSignMachine {
); );
self.signable.prepare_transaction( self.signable.prepare_transaction(
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"bulletproofs")),
&commitments, &commitments,
Bulletproofs::prove(
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"bulletproofs")),
&commitments,
)
.unwrap(),
) )
}; };

View file

@ -10,7 +10,7 @@ use monero::{
}; };
use monero_serai::{ use monero_serai::{
random_scalar, Protocol, random_scalar,
rpc::{EmptyResponse, RpcError, Rpc}, rpc::{EmptyResponse, RpcError, Rpc},
}; };
@ -29,8 +29,10 @@ pub async fn rpc() -> Rpc {
) )
.to_string(); .to_string();
// Mine 10 blocks so we have 10 decoys so decoy selection doesn't fail // Mine 20 blocks to ensure decoy availability
mine_block(&rpc, &addr).await.unwrap(); mine_block(&rpc, &addr).await.unwrap();
mine_block(&rpc, &addr).await.unwrap();
assert!(!matches!(rpc.get_protocol().await.unwrap(), Protocol::Unsupported));
rpc rpc
} }

View file

@ -115,12 +115,12 @@ async fn send_core(test: usize, multisig: bool) {
continue; continue;
} }
// We actually need 80 decoys for this transaction, so mine until then // We actually need 120 decoys for this transaction, so mine until then
// 80 + 60 (miner TX maturity) + 10 (lock blocks) // 120 + 60 (miner TX maturity) + 10 (lock blocks)
// It is possible for this to be lower, by noting maturity is sufficient regardless of lock // It is possible for this to be lower, by noting maturity is sufficient regardless of lock
// blocks, yet that's not currently implemented // blocks, yet that's not currently implemented
// TODO, if we care // TODO, if we care
while rpc.get_height().await.unwrap() < 160 { while rpc.get_height().await.unwrap() < 200 {
mine_block(&rpc, &addr.to_string()).await.unwrap(); mine_block(&rpc, &addr.to_string()).await.unwrap();
} }
@ -132,9 +132,14 @@ async fn send_core(test: usize, multisig: bool) {
} }
} }
let mut signable = let mut signable = SignableTransaction::new(
SignableTransaction::new(outputs, vec![(addr, amount - 10000000000)], Some(addr), fee) rpc.get_protocol().await.unwrap(),
.unwrap(); outputs,
vec![(addr, amount - 10000000000)],
Some(addr),
fee,
)
.unwrap();
if !multisig { if !multisig {
tx = Some(signable.sign(&mut OsRng, &rpc, &spend).await.unwrap()); tx = Some(signable.sign(&mut OsRng, &rpc, &spend).await.unwrap());

View file

@ -150,6 +150,7 @@ impl Coin for Monero {
transcript, transcript,
height, height,
MSignableTransaction::new( MSignableTransaction::new(
self.rpc.get_protocol().await.unwrap(), // TODO: Make this deterministic
inputs.drain(..).map(|input| input.0).collect(), inputs.drain(..).map(|input| input.0).collect(),
payments.to_vec(), payments.to_vec(),
Some(self.address(spend)), Some(self.address(spend)),
@ -231,8 +232,9 @@ impl Coin for Monero {
.ignore_timelock(); .ignore_timelock();
let amount = outputs[0].commitment.amount; let amount = outputs[0].commitment.amount;
let fee = 1000000000; // TODO let fee = 3000000000; // TODO
let tx = MSignableTransaction::new( let tx = MSignableTransaction::new(
self.rpc.get_protocol().await.unwrap(),
outputs, outputs,
vec![(address, amount - fee)], vec![(address, amount - fee)],
Some(self.empty_address()), Some(self.empty_address()),