From 797348be74e723cb5faad794525c6a706013a582 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 12 Nov 2023 07:49:10 -0500 Subject: [PATCH] Misc improvements and bug fixes --- coins/monero/src/block.rs | 6 +- coins/monero/src/lib.rs | 6 +- coins/monero/src/ring_signatures.rs | 17 +- coins/monero/src/ringct/borromean.rs | 12 +- coins/monero/src/ringct/mlsag.rs | 183 +++++++++++---------- coins/monero/src/rpc/mod.rs | 4 +- coins/monero/src/tests/mod.rs | 1 + coins/monero/src/tests/unreduced_scalar.rs | 32 ++++ coins/monero/src/transaction.rs | 2 +- coins/monero/src/unreduced_scalar.rs | 114 +++++-------- 10 files changed, 195 insertions(+), 182 deletions(-) create mode 100644 coins/monero/src/tests/unreduced_scalar.rs diff --git a/coins/monero/src/block.rs b/coins/monero/src/block.rs index ba716341c..2bced983b 100644 --- a/coins/monero/src/block.rs +++ b/coins/monero/src/block.rs @@ -79,8 +79,10 @@ impl Block { merkle_root(self.miner_tx.hash(), &self.txs) } - /// Serializes the block into the format required to get the proof of work hash, this is different - /// to the format for the block hash, to get the block hash use the [`Block::hash`] function. + /// Serialize the block as required for the proof of work hash. + /// + /// This is distinct from the serialization required for the block hash. To get the block hash, + /// use the [`Block::hash`] function. pub fn serialize_hashable(&self) -> Vec { let mut blob = self.header.serialize(); blob.extend_from_slice(&self.tx_merkle_root()); diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 1c7099c30..a33e4049d 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -26,13 +26,13 @@ use serialize::{read_byte, read_u16}; /// UnreducedScalar struct with functionality for recovering incorrectly reduced scalars. mod unreduced_scalar; +/// Ring Signature structs and functionality. +pub mod ring_signatures; + /// RingCT structs and functionality. pub mod ringct; use ringct::RctType; -/// Ring Signature structs with verifying functions. -pub mod ring_signatures; - /// Transaction structs. pub mod transaction; /// Block structs. diff --git a/coins/monero/src/ring_signatures.rs b/coins/monero/src/ring_signatures.rs index d720aa5d6..72d30b0e9 100644 --- a/coins/monero/src/ring_signatures.rs +++ b/coins/monero/src/ring_signatures.rs @@ -3,13 +3,15 @@ use std_shims::{ vec::Vec, }; +use zeroize::Zeroize; + use curve25519_dalek::{EdwardsPoint, Scalar}; use monero_generators::hash_to_point; use crate::{serialize::*, hash_to_scalar}; -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct Signature { c: Scalar, r: Scalar, @@ -27,7 +29,7 @@ impl Signature { } } -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct RingSignature { sigs: Vec, } @@ -44,17 +46,12 @@ impl RingSignature { Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? }) } - pub fn verify_ring_signature( - &self, - msg: &[u8; 32], - ring: &[EdwardsPoint], - key_image: &EdwardsPoint, - ) -> bool { + pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool { if ring.len() != self.sigs.len() { return false; } - let mut buf = Vec::with_capacity(32 + 32 * 2 * ring.len()); + let mut buf = Vec::with_capacity(32 + (32 * 2 * ring.len())); buf.extend_from_slice(msg); let mut sum = Scalar::ZERO; @@ -64,7 +61,7 @@ impl RingSignature { let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.r); buf.extend_from_slice(Li.compress().as_bytes()); #[allow(non_snake_case)] - let Ri = sig.r * hash_to_point(ring_member.compress().to_bytes()) + sig.c * key_image; + let Ri = (sig.r * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image); buf.extend_from_slice(Ri.compress().as_bytes()); sum += sig.c; diff --git a/coins/monero/src/ringct/borromean.rs b/coins/monero/src/ringct/borromean.rs index 214029477..215b3394c 100644 --- a/coins/monero/src/ringct/borromean.rs +++ b/coins/monero/src/ringct/borromean.rs @@ -1,7 +1,7 @@ use core::fmt::Debug; use std_shims::io::{self, Read, Write}; -use curve25519_dalek::{EdwardsPoint, Scalar, traits::Identity}; +use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint}; use monero_generators::H_pow_2; @@ -9,11 +9,9 @@ use crate::{hash_to_scalar, unreduced_scalar::UnreducedScalar, serialize::*}; /// 64 Borromean ring signatures. /// -/// This type keeps the data as raw bytes as Monero has some transactions with unreduced scalars in -/// this field. While we could use `from_bytes_mod_order`, we'd then not be able to encode this -/// back into it's original form. -/// -/// Those scalars also have a custom reduction algorithm... +/// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced. +/// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction +/// algorithm which was in use. #[derive(Clone, PartialEq, Eq, Debug)] pub struct BorromeanSignatures { pub s0: [UnreducedScalar; 64], @@ -56,7 +54,7 @@ impl BorromeanSignatures { &keys_b[i], &self.s1[i].recover_monero_slide_scalar(), ); - transcript[i * 32 .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes()); + transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes()); } hash_to_scalar(&transcript) == self.ee diff --git a/coins/monero/src/ringct/mlsag.rs b/coins/monero/src/ringct/mlsag.rs index d74811998..d3b4080e7 100644 --- a/coins/monero/src/ringct/mlsag.rs +++ b/coins/monero/src/ringct/mlsag.rs @@ -3,7 +3,9 @@ use std_shims::{ io::{self, Read, Write}, }; -use curve25519_dalek::{Scalar, EdwardsPoint}; +use zeroize::Zeroize; + +use curve25519_dalek::{traits::IsIdentity, Scalar, EdwardsPoint}; use monero_generators::H; @@ -16,11 +18,65 @@ pub enum MlsagError { InvalidRing, #[cfg_attr(feature = "std", error("invalid amount of key images"))] InvalidAmountOfKeyImages, + #[cfg_attr(feature = "std", error("invalid ss"))] + InvalidSs, + #[cfg_attr(feature = "std", error("key image was identity"))] + IdentityKeyImage, #[cfg_attr(feature = "std", error("invalid ci"))] InvalidCi, } -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct RingMatrix { + matrix: Vec>, +} + +impl RingMatrix { + pub fn new(matrix: Vec>) -> Result { + if matrix.is_empty() { + Err(MlsagError::InvalidRing)?; + } + for member in &matrix { + if member.is_empty() || (member.len() != matrix[0].len()) { + Err(MlsagError::InvalidRing)?; + } + } + + Ok(RingMatrix { matrix }) + } + + /// Construct a ring matrix for an individual output. + pub fn individual( + ring: &[[EdwardsPoint; 2]], + pseudo_out: EdwardsPoint, + ) -> Result { + let mut matrix = Vec::with_capacity(ring.len()); + for ring_member in ring { + matrix.push(vec![ring_member[0], ring_member[1] - pseudo_out]); + } + RingMatrix::new(matrix) + } + + pub fn iter(&self) -> impl Iterator { + self.matrix.iter().map(AsRef::as_ref) + } + + /// Return the amount of members in the ring. + pub fn members(&self) -> usize { + self.matrix.len() + } + + /// Returns the length of a ring member. + /// + /// A ring member is a vector of points for which the signer knows all of the discrete logarithms + /// of. + pub fn member_len(&self) -> usize { + // this is safe to do as the constructors don't allow empty rings + self.matrix[0].len() + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct Mlsag { pub ss: Vec>, pub cc: Scalar, @@ -47,12 +103,12 @@ impl Mlsag { &self, msg: &[u8; 32], ring: &RingMatrix, - key_images: &[&EdwardsPoint], + key_images: &[EdwardsPoint], ) -> Result<(), MlsagError> { - // Mlsag allows for layers to not need link-ability hence they don't need key images + // Mlsag allows for layers to not need linkability, hence they don't need key images // Monero requires that there is always only 1 non-linkable layer - the amount commitments. - if ring.member_len() != key_images.len() + 1 { - return Err(MlsagError::InvalidAmountOfKeyImages); + if ring.member_len() != (key_images.len() + 1) { + Err(MlsagError::InvalidAmountOfKeyImages)?; } let mut buf = Vec::with_capacity(6 * 32); @@ -62,9 +118,17 @@ impl Mlsag { // This is an iterator over the key images as options with an added entry of `None` at the // end for the non-linkable layer - let key_images_iter = key_images.iter().map(|ki| Some(*ki)).chain(Some(None)); + let key_images_iter = key_images.iter().map(|ki| Some(*ki)).chain(core::iter::once(None)); + + if ring.matrix.len() != self.ss.len() { + Err(MlsagError::InvalidSs)?; + } for (ring_member, ss) in ring.iter().zip(&self.ss) { + if ring_member.len() != ss.len() { + Err(MlsagError::InvalidSs)?; + } + for ((ring_member_entry, s), ki) in ring_member.iter().zip(ss).zip(key_images_iter.clone()) { #[allow(non_snake_case)] let L = EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, ring_member_entry, s); @@ -75,6 +139,10 @@ impl Mlsag { // Not all dimensions need to be linkable, e.g. commitments, and only linkable layers need // to have key images. if let Some(ki) = ki { + if ki.is_identity() { + Err(MlsagError::IdentityKeyImage)?; + } + #[allow(non_snake_case)] let R = (s * hash_to_point(ring_member_entry)) + (ci * ki); buf.extend_from_slice(R.compress().as_bytes()); @@ -84,113 +152,62 @@ impl Mlsag { ci = hash_to_scalar(&buf); // keep the msg in the buffer. buf.drain(msg.len() ..); - - if ci == Scalar::ZERO { - return Err(MlsagError::InvalidCi); - } - } - - if ci == self.cc { - Ok(()) - } else { - Err(MlsagError::InvalidCi) - } - } -} - -pub struct RingMatrix { - matrix: Vec>, -} - -impl RingMatrix { - /// Construct a simple ring matrix. - pub fn simple(ring: &[[EdwardsPoint; 2]], pseudo_out: EdwardsPoint) -> Result { - if ring.is_empty() { - return Err(MlsagError::InvalidRing); } - let mut matrix = Vec::with_capacity(ring.len()); - - for ring_member in ring { - matrix.push(vec![ring_member[0], ring_member[1] - pseudo_out]) + if ci != self.cc { + Err(MlsagError::InvalidCi)? } - - Ok(RingMatrix { matrix }) - } - - /// Returns a builder that can be used to construct an aggregate ring matrix - pub fn aggregate_builder(commitments: &[EdwardsPoint], fee: u64) -> AggregateRingMatrix { - AggregateRingMatrix::new(commitments, fee) - } - - pub fn iter(&self) -> impl Iterator { - self.matrix.iter().map(|ring_member| ring_member.as_slice()) - } - - /// Returns the length of one ring member, a ring member is a set of keys - /// that are linked, one of which are the real spends. - pub fn member_len(&self) -> usize { - // this is safe to do as the constructors don't allow empty rings - self.matrix[0].len() + Ok(()) } } -/// An aggregate ring matrix builder, used to set up the ring matrix to prove/ -/// verify an aggregate signature. -pub struct AggregateRingMatrix { +/// An aggregate ring matrix builder, usable to set up the ring matrix to prove/verify an aggregate +/// MLSAG signature. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct AggregateRingMatrixBuilder { key_ring: Vec>, amounts_ring: Vec, - sum_out_commitments: EdwardsPoint, - fee: EdwardsPoint, + sum_out: EdwardsPoint, } -impl AggregateRingMatrix { +impl AggregateRingMatrixBuilder { + /// Create a new AggregateRingMatrixBuilder. + /// + /// Takes in the transaction's outputs; commitments and fee. pub fn new(commitments: &[EdwardsPoint], fee: u64) -> Self { - AggregateRingMatrix { + AggregateRingMatrixBuilder { key_ring: vec![], amounts_ring: vec![], - sum_out_commitments: commitments.iter().sum::(), - fee: H() * Scalar::from(fee), + sum_out: commitments.iter().sum::() + (H() * Scalar::from(fee)), } } - /// push a ring, aka input, to this aggregate ring matrix. + /// Push a ring of [output key, commitment] to the matrix. pub fn push_ring(&mut self, ring: &[[EdwardsPoint; 2]]) -> Result<(), MlsagError> { - if self.amounts_ring.is_empty() { - // This is our fist ring, now we know the length of the decoys fill the - // `amounts_ring` table, so we don't have to loop back over and take - // these values off at the end. - self.amounts_ring = vec![-self.sum_out_commitments - self.fee; ring.len()]; + if self.key_ring.is_empty() { + self.key_ring = vec![vec![]; ring.len()]; + // Now that we know the length of the ring, fill the `amounts_ring`. + self.amounts_ring = vec![-self.sum_out; ring.len()]; } - if self.amounts_ring.len() != ring.len() || ring.is_empty() { + if (self.amounts_ring.len() != ring.len()) || ring.is_empty() { // All the rings in an aggregate matrix must be the same length. return Err(MlsagError::InvalidRing); } for (i, ring_member) in ring.iter().enumerate() { - if let Some(entry) = self.key_ring.get_mut(i) { - entry.push(ring_member[0]); - } else { - self.key_ring.push(vec![ring_member[0]]) - } - + self.key_ring[i].push(ring_member[0]); self.amounts_ring[i] += ring_member[1] } Ok(()) } - /// Finalize and return the [`RingMatrix`] - /// - /// This will panic if no rings have been added. - pub fn finish(mut self) -> RingMatrix { - assert!(!self.key_ring.is_empty(), "No ring members entered, can't build empty ring"); - + /// Build and return the [`RingMatrix`] + pub fn build(mut self) -> Result { for (i, amount_commitment) in self.amounts_ring.drain(..).enumerate() { - self.key_ring[i].push(amount_commitment) + self.key_ring[i].push(amount_commitment); } - - RingMatrix { matrix: self.key_ring } + RingMatrix::new(self.key_ring) } } diff --git a/coins/monero/src/rpc/mod.rs b/coins/monero/src/rpc/mod.rs index ecc32b034..df1dfb6f4 100644 --- a/coins/monero/src/rpc/mod.rs +++ b/coins/monero/src/rpc/mod.rs @@ -317,7 +317,7 @@ impl Rpc { .map_err(|_| RpcError::InvalidNode("invalid block"))?; // Make sure this is actually the block for this number - match block.miner_tx.prefix.inputs.first(0) { + match block.miner_tx.prefix.inputs.first() { Some(Input::Gen(actual)) => { if usize::try_from(*actual).unwrap() == number { Ok(block) @@ -325,7 +325,7 @@ impl Rpc { Err(RpcError::InvalidNode("different block than requested (number)")) } } - Err(RpcError::InvalidNode("block's miner_tx didn't have an input of kind Input::Gen")), + _ => Err(RpcError::InvalidNode("block's miner_tx didn't have an input of kind Input::Gen")), } } diff --git a/coins/monero/src/tests/mod.rs b/coins/monero/src/tests/mod.rs index 835b77ca9..64e725005 100644 --- a/coins/monero/src/tests/mod.rs +++ b/coins/monero/src/tests/mod.rs @@ -1,3 +1,4 @@ +mod unreduced_scalar; mod clsag; mod bulletproofs; mod address; diff --git a/coins/monero/src/tests/unreduced_scalar.rs b/coins/monero/src/tests/unreduced_scalar.rs new file mode 100644 index 000000000..1816991d9 --- /dev/null +++ b/coins/monero/src/tests/unreduced_scalar.rs @@ -0,0 +1,32 @@ +use curve25519_dalek::scalar::Scalar; + +use crate::unreduced_scalar::*; + +#[test] +fn recover_scalars() { + let test_recover = |stored: &str, recovered: &str| { + let stored = UnreducedScalar(hex::decode(stored).unwrap().try_into().unwrap()); + let recovered = + Scalar::from_canonical_bytes(hex::decode(recovered).unwrap().try_into().unwrap()).unwrap(); + assert_eq!(stored.recover_monero_slide_scalar(), recovered); + }; + + // https://www.moneroinflation.com/static/data_py/report_scalars_df.pdf + // Table 4. + test_recover( + "cb2be144948166d0a9edb831ea586da0c376efa217871505ad77f6ff80f203f8", + "b8ffd6a1aee47828808ab0d4c8524cb5c376efa217871505ad77f6ff80f20308", + ); + test_recover( + "343d3df8a1051c15a400649c423dc4ed58bef49c50caef6ca4a618b80dee22f4", + "21113355bc682e6d7a9d5b3f2137a30259bef49c50caef6ca4a618b80dee2204", + ); + test_recover( + "c14f75d612800ca2c1dcfa387a42c9cc086c005bc94b18d204dd61342418eba7", + "4f473804b1d27ab2c789c80ab21d034a096c005bc94b18d204dd61342418eb07", + ); + test_recover( + "000102030405060708090a0b0c0d0e0f826c4f6e2329a31bc5bc320af0b2bcbb", + "a124cfd387f461bf3719e03965ee6877826c4f6e2329a31bc5bc320af0b2bc0b", + ); +} diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index ec6cb7eab..23f9dd7f2 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -11,8 +11,8 @@ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; use crate::{ Protocol, hash, serialize::*, - ringct::{bulletproofs::Bulletproofs, RctType, RctBase, RctPrunable, RctSignatures}, ring_signatures::RingSignature, + ringct::{bulletproofs::Bulletproofs, RctType, RctBase, RctPrunable, RctSignatures}, }; #[derive(Clone, PartialEq, Eq, Debug)] diff --git a/coins/monero/src/unreduced_scalar.rs b/coins/monero/src/unreduced_scalar.rs index 715eedf82..500c23f11 100644 --- a/coins/monero/src/unreduced_scalar.rs +++ b/coins/monero/src/unreduced_scalar.rs @@ -1,27 +1,29 @@ use core::cmp::Ordering; -use std_shims::io::{self, *}; -use std_shims::sync::OnceLock; +use std_shims::{ + sync::OnceLock, + io::{self, *}, +}; use curve25519_dalek::scalar::Scalar; use crate::serialize::*; static PRECOMPUTED_SCALARS_CELL: OnceLock<[Scalar; 8]> = OnceLock::new(); -/// Precomputed scalars used to recover an incorrectly reduced scalar +/// Precomputed scalars used to recover an incorrectly reduced scalar. #[allow(non_snake_case)] -pub fn PRECOMPUTED_SCALARS() -> [Scalar; 8] { +pub(crate) fn PRECOMPUTED_SCALARS() -> [Scalar; 8] { *PRECOMPUTED_SCALARS_CELL.get_or_init(|| { let mut precomputed_scalars = [Scalar::ONE; 8]; for (i, scalar) in precomputed_scalars.iter_mut().enumerate().skip(1) { - *scalar = Scalar::from((i * 2 + 1) as u8); + *scalar = Scalar::from(((i * 2) + 1) as u8); } precomputed_scalars }) } #[derive(Clone, PartialEq, Eq, Debug)] -pub struct UnreducedScalar([u8; 32]); +pub struct UnreducedScalar(pub [u8; 32]); impl UnreducedScalar { pub fn write(&self, w: &mut W) -> io::Result<()> { @@ -36,14 +38,10 @@ impl UnreducedScalar { &self.0 } - pub fn reduce_mod_order(&self) -> Scalar { - Scalar::from_bytes_mod_order(self.0) - } - - fn as_bits(&self) -> [i8; 256] { + fn as_bits(&self) -> [u8; 256] { let mut bits = [0; 256]; for (i, bit) in bits.iter_mut().enumerate() { - *bit = 1 & (self.0[i >> 3] >> (i & 7)) as i8 + *bit = core::hint::black_box(1 & (self.0[i / 8] >> (i % 8))) } bits @@ -51,44 +49,50 @@ impl UnreducedScalar { /// Computes the non-adjacent form of this scalar with width 5. /// - /// This is the same as Monero's `slide` function, it intentionally gives incorrect - /// outputs if the last bit is set to match Monero. + /// This matches Monero's `slide` function and intentionally gives incorrect outputs under + /// certain conditions in order to match Monero. + /// + /// This function does not execute in constant time. fn non_adjacent_form(&self) -> [i8; 256] { - let mut bits = self.as_bits(); + let bits = self.as_bits(); + let mut naf = [0i8; 256]; + for (b, bit) in bits.into_iter().enumerate() { + naf[b] = bit as i8; + } - #[allow(clippy::needless_range_loop)] for i in 0 .. 256 { - if bits[i] != 0 { + if naf[i] != 0 { // if the bit is a one, work our way up through the window // combining the bits with this bit. for b in 1 .. 6 { - if i + b >= 256 { + if (i + b) >= 256 { // if we are at the length of the array then break out // the loop. break; } // potential_carry - the value of the bit at i+b compared to the bit at i - let potential_carry = bits[i + b] << b; + let potential_carry = naf[i + b] << b; if potential_carry != 0 { - if bits[i] + potential_carry <= 15 { + if (naf[i] + potential_carry) <= 15 { // if our current "bit" plus the potential carry is less than 16 // add it to our current "bit" and set the potential carry bit to 0. - bits[i] += potential_carry; - bits[i + b] = 0; - } else if bits[i] - potential_carry >= -15 { + naf[i] += potential_carry; + naf[i + b] = 0; + } else if (naf[i] - potential_carry) >= -15 { // else if our current "bit" minus the potential carry is more than -16 // take it away from our current "bit". // we then work our way up through the bits setting ones to zero, when // we hit the first zero we change it to one then stop, this is to factor // in the minus. - bits[i] -= potential_carry; - for k in i + b .. 256 { - if bits[k] == 0 { - bits[k] = 1; + naf[i] -= potential_carry; + #[allow(clippy::needless_range_loop)] + for k in (i + b) .. 256 { + if naf[k] == 0 { + naf[k] = 1; break; } - bits[k] = 0; + naf[k] = 0; } } else { break; @@ -98,10 +102,11 @@ impl UnreducedScalar { } } - bits + naf } - /// Recover the scalar that an array of bytes was incorrectly interpreted as. + /// Recover the scalar that an array of bytes was incorrectly interpreted as by Monero's `slide` + /// function. /// /// In Borromean range proofs Monero was not checking that the scalars used were /// reduced. This lead to the scalar stored being interpreted as a different scalar, @@ -111,61 +116,22 @@ impl UnreducedScalar { pub fn recover_monero_slide_scalar(&self) -> Scalar { if self.0[31] & 128 == 0 { // Computing the w-NAF of a number can only give an output with 1 more bit than - // the number so even if the number isn't reduced the `slide` function will be + // the number, so even if the number isn't reduced, the `slide` function will be // correct when the last bit isn't set. - return self.reduce_mod_order(); + return Scalar::from_bytes_mod_order(self.0); } - let naf = self.non_adjacent_form(); - let precomputed_scalars = PRECOMPUTED_SCALARS(); let mut recovered = Scalar::ZERO; - - for &numb in naf.iter().rev() { + for &numb in self.non_adjacent_form().iter().rev() { recovered += recovered; - match numb.cmp(&0) { - Ordering::Greater => recovered += precomputed_scalars[numb as usize / 2], - Ordering::Less => recovered -= precomputed_scalars[(-numb) as usize / 2], + Ordering::Greater => recovered += precomputed_scalars[(numb as usize) / 2], + Ordering::Less => recovered -= precomputed_scalars[((-numb) as usize) / 2], Ordering::Equal => (), } } - recovered } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn recover_scalars() { - let test_recover = |stored: &str, recovered: &str| { - let stored = UnreducedScalar(hex::decode(stored).unwrap().try_into().unwrap()); - let recovered = - Scalar::from_canonical_bytes(hex::decode(recovered).unwrap().try_into().unwrap()).unwrap(); - assert_eq!(stored.recover_monero_slide_scalar(), recovered); - }; - - // https://www.moneroinflation.com/static/data_py/report_scalars_df.pdf - // Table 4. - test_recover( - "cb2be144948166d0a9edb831ea586da0c376efa217871505ad77f6ff80f203f8", - "b8ffd6a1aee47828808ab0d4c8524cb5c376efa217871505ad77f6ff80f20308", - ); - test_recover( - "343d3df8a1051c15a400649c423dc4ed58bef49c50caef6ca4a618b80dee22f4", - "21113355bc682e6d7a9d5b3f2137a30259bef49c50caef6ca4a618b80dee2204", - ); - test_recover( - "c14f75d612800ca2c1dcfa387a42c9cc086c005bc94b18d204dd61342418eba7", - "4f473804b1d27ab2c789c80ab21d034a096c005bc94b18d204dd61342418eb07", - ); - test_recover( - "000102030405060708090a0b0c0d0e0f826c4f6e2329a31bc5bc320af0b2bcbb", - "a124cfd387f461bf3719e03965ee6877826c4f6e2329a31bc5bc320af0b2bc0b", - ); - } -}