From 023afaf7cef8cb67c588d9fac08b7cfba685032a Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Wed, 27 Jul 2022 04:05:43 -0500 Subject: [PATCH] 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 --- .github/actions/monero/action.yml | 6 +- coins/monero/src/lib.rs | 26 ++ coins/monero/src/ringct/bulletproofs/core.rs | 243 +++++++++++++----- coins/monero/src/ringct/bulletproofs/mod.rs | 30 ++- .../src/ringct/bulletproofs/scalar_vector.rs | 21 +- coins/monero/src/ringct/clsag/mod.rs | 8 +- coins/monero/src/ringct/mod.rs | 27 +- coins/monero/src/rpc.rs | 32 +++ coins/monero/src/transaction.rs | 16 +- coins/monero/src/wallet/decoys.rs | 18 +- coins/monero/src/wallet/send/mod.rs | 54 ++-- coins/monero/src/wallet/send/multisig.rs | 8 +- coins/monero/tests/rpc.rs | 6 +- coins/monero/tests/send.rs | 17 +- processor/src/coin/monero.rs | 4 +- 15 files changed, 384 insertions(+), 132 deletions(-) diff --git a/.github/actions/monero/action.yml b/.github/actions/monero/action.yml index 6a5b3217..cb6aa965 100644 --- a/.github/actions/monero/action.yml +++ b/.github/actions/monero/action.yml @@ -9,7 +9,7 @@ runs: uses: actions/cache@v3 with: 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 if: steps.cache-monerod.outputs.cache-hit != 'true' @@ -27,11 +27,11 @@ runs: RUNNER_OS=linux 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 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 shell: bash diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 18990dad..daf56aaf 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -25,6 +25,32 @@ pub mod wallet; #[cfg(test)] 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! { static ref H: EdwardsPoint = CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes())) diff --git a/coins/monero/src/ringct/bulletproofs/core.rs b/coins/monero/src/ringct/bulletproofs/core.rs index e1ad6bf8..fdae97da 100644 --- a/coins/monero/src/ringct/bulletproofs/core.rs +++ b/coins/monero/src/ringct/bulletproofs/core.rs @@ -7,12 +7,16 @@ use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::{scalar::Scalar as DalekScalar, edwards::EdwardsPoint as DalekPoint}; 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::{ - 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::*}, serialize::write_varint, }; @@ -23,10 +27,6 @@ lazy_static! { static ref H: EdwardsPoint = EdwardsPoint(*DALEK_H); } -fn random_scalar(rng: &mut R) -> Scalar { - Scalar(dalek_random(rng)) -} - fn hash_to_scalar(data: &[u8]) -> Scalar { Scalar(dalek_hash(data)) } @@ -36,12 +36,6 @@ pub(crate) const MAX_M: usize = 16; const N: usize = 64; 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 { G: Vec, H: Vec, @@ -64,6 +58,7 @@ fn generators_core(prefix: &'static [u8]) -> Generators { 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 { debug_assert_eq!(a.len(), 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) { 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::>(); let mut aL = 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::>()) } -fn alpha( +fn alpha_rho( rng: &mut R, generators: &Generators, aL: &ScalarVector, aR: &ScalarVector, ) -> (Scalar, EdwardsPoint) { - let alpha = random_scalar(&mut *rng); - (alpha, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * alpha)) * *INV_EIGHT) + let ar = Scalar::random(rng); + (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::>(); + res.push((cL, U)); + res +} + +lazy_static! { + static ref TWO_N: ScalarVector = ScalarVector::powers(Scalar::from(2u8), N); } // Bulletproofs-specific lazy_static! { 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 lazy_static! { static ref GENERATORS_PLUS: Generators = generators_core(b"bulletproof_plus"); - static ref TRANSCRIPT_PLUS: EdwardsPoint = - EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))); + static ref TRANSCRIPT_PLUS: [u8; 32] = + EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))).compress().to_bytes(); } -fn even_powers_sum(x: Scalar, pow: usize) -> Scalar { - 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 = xsq; - - let mut prev = 2; - while prev < pow { - res += res * xsq; - prev += 2; - } - - res +// TRANSCRIPT_PLUS isn't a Scalar, so we need this alternative for the first hash +fn hash_plus(mash: &[[u8; 32]]) -> Scalar { + let slice = + &[&*TRANSCRIPT_PLUS as &[u8], mash.iter().cloned().flatten().collect::>().as_ref()] + .concat(); + hash_to_scalar(slice) } // Types for all Bulletproofs +#[allow(clippy::large_enum_variant)] #[derive(Clone, PartialEq, Eq, Debug)] pub enum Bulletproofs { Original { @@ -173,6 +185,17 @@ pub enum Bulletproofs { b: DalekScalar, t: DalekScalar, }, + + Plus { + A: DalekPoint, + A1: DalekPoint, + B: DalekPoint, + r1: DalekScalar, + s1: DalekScalar, + d1: DalekScalar, + L: Vec, + R: Vec, + }, } pub(crate) fn prove( @@ -183,12 +206,11 @@ pub(crate) fn prove( let (aL, aR) = bit_decompose(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) = - ScalarVector((0 .. (MN * 2)).map(|_| random_scalar(&mut *rng)).collect::>()).split(); - let rho = random_scalar(&mut *rng); - let S = (vector_exponent(&GENERATORS, &sL, &sR) + (EdwardsPoint::generator() * rho)) * *INV_EIGHT; + ScalarVector((0 .. (MN * 2)).map(|_| Scalar::random(&mut *rng)).collect::>()).split(); + let (rho, S) = alpha_rho(&mut *rng, &GENERATORS, &sL, &sR); let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]); let mut cache = hash_to_scalar(&y.to_bytes()); @@ -212,19 +234,18 @@ pub(crate) fn prove( let t1 = inner_product(&l0, &r1) + inner_product(&l1, &r0); let t2 = inner_product(&l1, &r1); - let tau1 = random_scalar(&mut *rng); - let tau2 = random_scalar(&mut *rng); + let tau1 = Scalar::random(&mut *rng); + let tau2 = Scalar::random(rng); - let T1 = multiexp(&[(t1, *H), (tau1, EdwardsPoint::generator())]) * *INV_EIGHT; - let T2 = multiexp(&[(t2, *H), (tau2, EdwardsPoint::generator())]) * *INV_EIGHT; + let T1 = prove_multiexp(&[(t1, *H), (tau1, EdwardsPoint::generator())]); + let T2 = prove_multiexp(&[(t2, *H), (tau2, EdwardsPoint::generator())]); let x = 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); - for i in 1 ..= gamma.len() { - taux += zpow[i + 1] * gamma[i - 1]; + for (i, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() { + taux += zpow[i + 2] * gamma; } let mu = (x * rho) + alpha; @@ -259,26 +280,8 @@ pub(crate) fn prove( 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_s = aL - .0 - .iter() - .cloned() - .zip(G_R.iter().cloned()) - .chain(bR.0.iter().cloned().zip(H_L.iter().cloned())) - .collect::>(); - 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::>(); - R_i_s.push((cR, U)); - let R_i = multiexp(&R_i_s) * *INV_EIGHT; - + let L_i = prove_multiexp(&LR_statements(&aL, G_R, &bR, H_L, cL, U)); + let R_i = prove_multiexp(&LR_statements(&aR, G_L, &bL, H_R, cR, U)); L.push(L_i); R.push(R_i); @@ -308,3 +311,113 @@ pub(crate) fn prove( t: *t, } } + +pub(crate) fn prove_plus( + 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(), + } +} diff --git a/coins/monero/src/ringct/bulletproofs/mod.rs b/coins/monero/src/ringct/bulletproofs/mod.rs index e1d1d815..58d8dc78 100644 --- a/coins/monero/src/ringct/bulletproofs/mod.rs +++ b/coins/monero/src/ringct/bulletproofs/mod.rs @@ -10,11 +10,12 @@ pub(crate) mod scalar_vector; mod core; 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; impl Bulletproofs { + // TODO 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; @@ -32,11 +33,12 @@ impl Bulletproofs { pub fn prove( rng: &mut R, outputs: &[Commitment], + plus: bool, ) -> Result { if outputs.len() > MAX_OUTPUTS { return Err(TransactionError::TooManyOutputs)?; } - Ok(prove(rng, outputs)) + Ok(if !plus { prove(rng, outputs) } else { prove_plus(rng, outputs) }) } fn serialize_core std::io::Result<()>>( @@ -58,6 +60,17 @@ impl Bulletproofs { write_scalar(b, 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)?, }) } + + pub fn deserialize_plus(r: &mut R) -> std::io::Result { + 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)?, + }) + } } diff --git a/coins/monero/src/ringct/bulletproofs/scalar_vector.rs b/coins/monero/src/ringct/bulletproofs/scalar_vector.rs index 51fd6065..a9e9c13e 100644 --- a/coins/monero/src/ringct/bulletproofs/scalar_vector.rs +++ b/coins/monero/src/ringct/bulletproofs/scalar_vector.rs @@ -60,6 +60,24 @@ impl ScalarVector { 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 { 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 { - (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 { diff --git a/coins/monero/src/ringct/clsag/mod.rs b/coins/monero/src/ringct/clsag/mod.rs index 8810f4b9..20763b64 100644 --- a/coins/monero/src/ringct/clsag/mod.rs +++ b/coins/monero/src/ringct/clsag/mod.rs @@ -12,8 +12,8 @@ use curve25519_dalek::{ }; use crate::{ - Commitment, random_scalar, hash_to_scalar, transaction::RING_LEN, wallet::decoys::Decoys, - ringct::hash_to_point, serialize::*, + Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys, ringct::hash_to_point, + serialize::*, }; #[cfg(feature = "multisig")] @@ -292,8 +292,8 @@ impl Clsag { Ok(()) } - pub(crate) fn fee_weight() -> usize { - (RING_LEN * 32) + 32 + 32 + pub(crate) fn fee_weight(ring_len: usize) -> usize { + (ring_len * 32) + 32 + 32 } pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { diff --git a/coins/monero/src/ringct/mod.rs b/coins/monero/src/ringct/mod.rs index c14b026f..be9293be 100644 --- a/coins/monero/src/ringct/mod.rs +++ b/coins/monero/src/ringct/mod.rs @@ -31,7 +31,7 @@ impl RctBase { w.write_all(&[rct_type])?; match rct_type { 0 => Ok(()), - 5 => { + 5 | 6 => { write_varint(&self.fee, w)?; for ecdh in &self.ecdh_info { w.write_all(ecdh)?; @@ -78,18 +78,24 @@ impl RctPrunable { pub fn rct_type(&self) -> u8 { match self { 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 { - 1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight() + 32)) + pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize) -> usize { + 1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight(ring_len) + 32)) } pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { match self { RctPrunable::Null => Ok(()), - RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { + RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs, .. } => { write_vec(Bulletproofs::serialize, bulletproofs, w)?; write_raw_vec(Clsag::serialize, clsags, w)?; write_raw_vec(write_point, pseudo_outs, w) @@ -104,8 +110,11 @@ impl RctPrunable { ) -> std::io::Result { Ok(match rct_type { 0 => RctPrunable::Null, - 5 => RctPrunable::Clsag { - bulletproofs: read_vec(Bulletproofs::deserialize, r)?, + 5 | 6 => RctPrunable::Clsag { + bulletproofs: read_vec( + if rct_type == 5 { Bulletproofs::deserialize } else { Bulletproofs::deserialize_plus }, + r, + )?, clsags: (0 .. decoys.len()) .map(|o| Clsag::deserialize(decoys[o], r)) .collect::>()?, @@ -135,8 +144,8 @@ 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(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize) -> usize { + RctBase::fee_weight(outputs) + RctPrunable::fee_weight(ring_len, inputs, outputs) } pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs index 133af376..5d1a793d 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc.rs @@ -10,6 +10,7 @@ use serde_json::json; use reqwest; use crate::{ + Protocol, transaction::{Input, Timelock, Transaction}, block::Block, wallet::Fee, @@ -98,6 +99,37 @@ impl Rpc { }) } + pub async fn get_protocol(&self) -> Result { + #[derive(Deserialize, Debug)] + struct ProtocolResponse { + major_version: usize, + } + + #[derive(Deserialize, Debug)] + struct LastHeaderResponse { + block_header: ProtocolResponse, + } + + Ok( + match self + .rpc_call::<_, JsonRpcResponse>( + "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 { #[derive(Deserialize, Debug)] struct HeightResponse { diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index a8e83649..53219b3e 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -8,8 +8,6 @@ use crate::{ ringct::{RctPrunable, RctSignatures}, }; -pub const RING_LEN: usize = 11; - #[derive(Clone, PartialEq, Eq, Debug)] pub enum Input { Gen(u64), @@ -19,10 +17,10 @@ pub enum Input { impl Input { // 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 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(&self, w: &mut W) -> std::io::Result<()> { @@ -161,11 +159,11 @@ pub struct 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 1 + 1 + varint_len(inputs) + - (inputs * Input::fee_weight()) + + (inputs * Input::fee_weight(ring_len)) + 1 + (outputs * Output::fee_weight()) + varint_len(extra) + @@ -205,9 +203,9 @@ 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(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize, extra: usize) -> usize { + TransactionPrefix::fee_weight(ring_len, inputs, outputs, extra) + + RctSignatures::fee_weight(ring_len, inputs, outputs) } pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { diff --git a/coins/monero/src/wallet/decoys.rs b/coins/monero/src/wallet/decoys.rs index 58229c8b..4a102564 100644 --- a/coins/monero/src/wallet/decoys.rs +++ b/coins/monero/src/wallet/decoys.rs @@ -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 = Gamma::new(19.28, 1.0 / 1.61).unwrap(); static ref DISTRIBUTION: Mutex> = Mutex::new(Vec::with_capacity(3000000)); @@ -109,9 +106,12 @@ impl Decoys { pub(crate) async fn select( rng: &mut R, rpc: &Rpc, + ring_len: usize, height: usize, inputs: &[SpendableOutput], ) -> Result, 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::>(); + let mut ring = decoys.drain((decoys.len() - decoy_count) ..).collect::>(); 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::>() { + for removed in ring.drain(0 .. (ring_len / 2)).collect::>() { // 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)); } diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index e994662f..b90a13e1 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -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( rng: &mut R, rpc: &Rpc, + ring_len: usize, inputs: &[SpendableOutput], spend: &Scalar, tx: &mut Transaction, @@ -113,6 +114,7 @@ async fn prepare_inputs( 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, payments: Vec<(Address, u64)>, outputs: Vec, @@ -167,6 +170,7 @@ pub struct SignableTransaction { impl SignableTransaction { pub fn new( + protocol: Protocol, inputs: Vec, mut payments: Vec<(Address, u64)>, change_address: Option
, @@ -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::(); @@ -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( @@ -259,7 +271,14 @@ impl SignableTransaction { (commitments, sum) } - fn prepare_transaction(&self, commitments: &[Commitment], bp: Bulletproofs) -> Transaction { + fn prepare_transaction( + &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 { diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index b5b58956..436273a9 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -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 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(), ) }; diff --git a/coins/monero/tests/rpc.rs b/coins/monero/tests/rpc.rs index 44906e77..c80a9e48 100644 --- a/coins/monero/tests/rpc.rs +++ b/coins/monero/tests/rpc.rs @@ -10,7 +10,7 @@ use monero::{ }; use monero_serai::{ - random_scalar, + Protocol, random_scalar, rpc::{EmptyResponse, RpcError, Rpc}, }; @@ -29,8 +29,10 @@ pub async fn rpc() -> Rpc { ) .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(); + assert!(!matches!(rpc.get_protocol().await.unwrap(), Protocol::Unsupported)); rpc } diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index 33b3bf64..0da7322b 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -115,12 +115,12 @@ async fn send_core(test: usize, multisig: bool) { continue; } - // We actually need 80 decoys for this transaction, so mine until then - // 80 + 60 (miner TX maturity) + 10 (lock blocks) + // We actually need 120 decoys for this transaction, so mine until then + // 120 + 60 (miner TX maturity) + 10 (lock blocks) // It is possible for this to be lower, by noting maturity is sufficient regardless of lock // blocks, yet that's not currently implemented // 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(); } @@ -132,9 +132,14 @@ async fn send_core(test: usize, multisig: bool) { } } - let mut signable = - SignableTransaction::new(outputs, vec![(addr, amount - 10000000000)], Some(addr), fee) - .unwrap(); + let mut signable = SignableTransaction::new( + rpc.get_protocol().await.unwrap(), + outputs, + vec![(addr, amount - 10000000000)], + Some(addr), + fee, + ) + .unwrap(); if !multisig { tx = Some(signable.sign(&mut OsRng, &rpc, &spend).await.unwrap()); diff --git a/processor/src/coin/monero.rs b/processor/src/coin/monero.rs index 38d61cad..39dbe9dd 100644 --- a/processor/src/coin/monero.rs +++ b/processor/src/coin/monero.rs @@ -150,6 +150,7 @@ impl Coin for Monero { transcript, height, MSignableTransaction::new( + self.rpc.get_protocol().await.unwrap(), // TODO: Make this deterministic inputs.drain(..).map(|input| input.0).collect(), payments.to_vec(), Some(self.address(spend)), @@ -231,8 +232,9 @@ impl Coin for Monero { .ignore_timelock(); let amount = outputs[0].commitment.amount; - let fee = 1000000000; // TODO + let fee = 3000000000; // TODO let tx = MSignableTransaction::new( + self.rpc.get_protocol().await.unwrap(), outputs, vec![(address, amount - fee)], Some(self.empty_address()),