13 Commits

Author SHA1 Message Date
hinto.janai
d4b22e5136 doc ringct/mod.rs 2024-05-16 19:06:59 -04:00
hinto.janai
58fe79da10 lib: doc constants 2024-05-16 18:11:20 -04:00
hinto.janai
3f07dd13c6 block: doc Block 2024-05-16 17:59:56 -04:00
hinto.janai
4e1d86dae2 block: doc BlockHeader 2024-05-16 17:45:15 -04:00
hinto.janai
7ef21830a5 readme: add feature flags 2024-05-10 15:35:39 -04:00
Luke Parker
8cb4c5d167 Various RingCT doc comments 2024-04-23 03:44:51 -04:00
Luke Parker
f9e4b420ed Move amount_decryption into EncryptedAmount::decrypt 2024-04-22 01:36:43 -04:00
Luke Parker
817b8e99d3 Remove experimental feature from monero-serai 2024-04-22 00:47:59 -04:00
Luke Parker
925cef17f2 Tidy Borromean/MLSAG a tad 2024-04-22 00:46:46 -04:00
Luke Parker
3283cd79e4 Remove the distribution cache
It's a notable bandwidth/performance improvement, yet it's not ready. We need a
dedicated Distribution struct which is managed by the wallet and passed in.
While we can do that now, it's not currently worth the effort.
2024-04-22 00:29:18 -04:00
Luke Parker
51e2f24bc1 Make CLSAG signing private
Also adds a bit more documentation and does a bit more tidying.
2024-04-22 00:25:58 -04:00
Luke Parker
372e29fe08 Rename Bulletproofs to Bulletproof, since they are a single Bulletproof
Also bifurcates prove with prove_plus, and adds a few documentation items.
2024-04-22 00:00:14 -04:00
Luke Parker
fccb1aea51 Remove unsafe creation of dalek_ff_group::EdwardsPoint in BP+ 2024-04-21 23:45:07 -04:00
24 changed files with 528 additions and 219 deletions

1
Cargo.lock generated
View File

@@ -4672,7 +4672,6 @@ dependencies = [
name = "monero-serai"
version = "0.1.4-alpha"
dependencies = [
"async-lock",
"async-trait",
"base58-monero",
"curve25519-dalek",

View File

@@ -47,8 +47,6 @@ frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8
monero-generators = { path = "generators", version = "0.4", default-features = false }
async-lock = { version = "3", default-features = false, optional = true }
hex-literal = "0.4"
hex = { version = "0.4", default-features = false, features = ["alloc"] }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
@@ -93,8 +91,6 @@ std = [
"monero-generators/std",
"async-lock?/std",
"hex/std",
"serde/std",
"serde_json/std",
@@ -102,10 +98,8 @@ std = [
"base58-monero/std",
]
cache-distribution = ["async-lock"]
http-rpc = ["digest_auth", "simple-request", "tokio"]
multisig = ["transcript", "frost", "std"]
binaries = ["tokio/rt-multi-thread", "tokio/macros", "http-rpc"]
experimental = []
default = ["std", "http-rpc"]

View File

@@ -47,3 +47,15 @@ It also won't act as a wallet, just as a transaction library. wallet2 has
several *non-transaction-level* policies, such as always attempting to use two
inputs to create transactions. These are considered out of scope to
monero-serai.
### Feature flags
monero-serai has certain functionality behind feature flags:
- `std:` Enables usage of Rust's `std` and several other functionality. See `Cargo.toml` for the full list.
- `http-rpc`: Enables an HTTP(S) transport type within the `rpc` module
- `multisig`: Enables multi-signature features within the `wallet` module
- `binaries`: TODO
The features enabled by default are:
- `std`
- `http-rpc`

View File

@@ -15,16 +15,43 @@ const CORRECT_BLOCK_HASH_202612: [u8; 32] =
const EXISTING_BLOCK_HASH_202612: [u8; 32] =
hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698");
/// The header of a [`Block`].
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct BlockHeader {
/// This represents the hardfork number of the block.
pub major_version: u8,
/// This field is used to vote for a particular [hardfork](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_basic/cryptonote_basic.h#L460).
pub minor_version: u8,
/// The UNIX time at which the block was mined.
pub timestamp: u64,
/// The previous [`Block::hash`].
pub previous: [u8; 32],
/// The block's nonce.
pub nonce: u32,
}
impl BlockHeader {
/// Serialize [`Self`] into the writer `w`.
///
/// # Example
/// ```rust
/// # use monero_serai::block::*;
/// # fn main() -> std::io::Result<()> {
/// let block_header = BlockHeader {
/// major_version: 1,
/// minor_version: 2,
/// timestamp: 3,
/// previous: [4; 32],
/// nonce: 5,
/// };
///
/// let mut writer = vec![];
/// block_header.write(&mut writer)?;
/// # }
/// ```
///
/// # Errors
/// This function returns any errors from the writer itself.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_varint(&self.major_version, w)?;
write_varint(&self.minor_version, w)?;
@@ -33,12 +60,58 @@ impl BlockHeader {
w.write_all(&self.nonce.to_le_bytes())
}
/// Serialize [`Self`] into a new byte buffer.
///
/// # Example
/// ```rust
/// # use monero_serai::block::*;
/// # fn main() -> std::io::Result<()> {
/// let block_header = BlockHeader {
/// major_version: 1,
/// minor_version: 2,
/// timestamp: 3,
/// previous: [4; 32],
/// nonce: 5,
/// };
///
/// let mut writer = vec![];
/// block_header.write(&mut writer)?;
///
/// let serialized = block_header.serialize();
/// assert_eq!(serialized, writer);
/// # }
/// ```
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
/// Create [`Self`] from the reader `r`.
///
/// # Example
/// ```rust
/// # use monero_serai::block::*;
/// # fn main() -> std::io::Result<()> {
/// let block_header = BlockHeader {
/// major_version: 1,
/// minor_version: 2,
/// timestamp: 3,
/// previous: [4; 32],
/// nonce: 5,
/// };
///
/// let mut vec = vec![];
/// block_header.write(&mut vec)?;
///
/// let read = BlockHeader::read(& vec)?;
/// assert_eq!(read, block_header);
/// # }
/// ```
///
/// # Errors
/// This function returns an error if either the reader failed,
/// or if the data could not be deserialized into a [`Self`].
pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> {
Ok(BlockHeader {
major_version: read_varint(r)?,
@@ -50,14 +123,19 @@ impl BlockHeader {
}
}
/// Block on the Monero blockchain.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Block {
/// The header of this block.
pub header: BlockHeader,
/// The miner/coinbase transaction.
pub miner_tx: Transaction,
/// Hashes of all the transactions within this block.
pub txs: Vec<[u8; 32]>,
}
impl Block {
/// Return the amount of Monero generated in this block in atomic units.
pub fn number(&self) -> Option<u64> {
match self.miner_tx.prefix.inputs.first() {
Some(Input::Gen(number)) => Some(*number),
@@ -65,6 +143,10 @@ impl Block {
}
}
/// Serialize [`Self`] into the writer `w`.
///
/// # Errors
/// This function returns any errors from the writer itself.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.header.write(w)?;
self.miner_tx.write(w)?;
@@ -75,6 +157,11 @@ impl Block {
Ok(())
}
/// Return the merkle root of this block.
///
/// In the case that this block has no transactions other than
/// the miner transaction, the miner transaction hash is returned,
/// i.e. the [`Transaction::hash`] of [`Self::miner_tx`] is returned.
fn tx_merkle_root(&self) -> [u8; 32] {
merkle_root(self.miner_tx.hash(), &self.txs)
}
@@ -91,6 +178,7 @@ impl Block {
blob
}
/// Calculate the hash of this block.
pub fn hash(&self) -> [u8; 32] {
let mut hashable = self.serialize_hashable();
// Monero pre-appends a VarInt of the block hashing blobs length before getting the block hash
@@ -107,12 +195,18 @@ impl Block {
hash
}
/// Serialize [`Self`] into a new byte buffer.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
/// Create [`Self`] from the reader `r`.
///
/// # Errors
/// This function returns an error if either the reader failed,
/// or if the data could not be deserialized into a [`Self`].
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
let header = BlockHeader::read(r)?;
@@ -124,7 +218,7 @@ impl Block {
Ok(Block {
header,
miner_tx,
txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
txs: (0_usize..read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
})
}
}

View File

@@ -51,8 +51,26 @@ pub mod wallet;
#[cfg(test)]
mod tests;
/// Default block lock time for transactions.
///
/// This is the amount of new blocks that must
/// pass before a new transaction can be spent.
///
/// Equivalent to Monero's [`CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE`](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_config.h#L49).
pub const DEFAULT_LOCK_WINDOW: usize = 10;
/// Block lock time for coinbase transactions.
///
/// This is the amount of new blocks that must
/// pass before a coinbase/miner transaction can be spent.
///
/// Equivalent to Monero's [`CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW`](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_config.h#L44).
pub const COINBASE_LOCK_WINDOW: usize = 60;
/// Average amount of seconds it takes for a block to be mined.
///
/// This is target amount of seconds mining difficulty will adjust to,
/// i.e. a block will be mined every `BLOCK_TIME` seconds on average.
///
/// Equivalent to Monero's [`DIFFICULTY_TARGET_V2`](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_config.h#L44).
pub const BLOCK_TIME: usize = 120;
static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new();
@@ -75,19 +93,34 @@ pub(crate) fn BASEPOINT_PRECOMP() -> &'static VartimeEdwardsPrecomputation {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
#[allow(non_camel_case_types)]
pub enum Protocol {
/// Version 14.
v14,
/// Version 16.
v16,
/// A custom version with customized properties.
Custom {
/// See [`Self::ring_len`].
ring_len: usize,
/// See [`Self::bp_plus`].
bp_plus: bool,
/// See [`Self::optimal_rct_type`].
optimal_rct_type: RctType,
/// See [`Self::view_tags`].
view_tags: bool,
/// See [`Self::v16_fee`].
v16_fee: bool,
},
}
impl Protocol {
/// Amount of ring members under this protocol version.
///
/// # Example
/// ```rust
/// # use monero_serai::*;
/// assert_eq!(Protocol::v14.ring_len(), 11);
/// assert_eq!(Protocol::v16.ring_len(), 16);
/// ```
pub fn ring_len(&self) -> usize {
match self {
Protocol::v14 => 11,
@@ -99,6 +132,13 @@ impl Protocol {
/// Whether or not the specified version uses Bulletproofs or Bulletproofs+.
///
/// This method will likely be reworked when versions not using Bulletproofs at all are added.
///
/// # Example
/// ```rust
/// # use monero_serai::*;
/// assert_eq!(Protocol::v14.bp_plus(), false);
/// assert_eq!(Protocol::v16.bp_plus(), true);
/// ```
pub fn bp_plus(&self) -> bool {
match self {
Protocol::v14 => false,
@@ -107,6 +147,14 @@ impl Protocol {
}
}
/// The optimal RingCT type for this version.
///
/// # Example
/// ```rust
/// # use monero_serai::{*, ringct::*};
/// assert_eq!(Protocol::v14.optimal_rct_type(), RctType::Clsag);
/// assert_eq!(Protocol::v16.optimal_rct_type(), RctType::BulletproofsPlus);
/// ```
// TODO: Make this an Option when we support pre-RCT protocols
pub fn optimal_rct_type(&self) -> RctType {
match self {
@@ -117,6 +165,13 @@ impl Protocol {
}
/// Whether or not the specified version uses view tags.
///
/// # Example
/// ```rust
/// # use monero_serai::{*, ringct::*};
/// assert_eq!(Protocol::v14.view_tags(), false);
/// assert_eq!(Protocol::v16.view_tags(), true);
/// ```
pub fn view_tags(&self) -> bool {
match self {
Protocol::v14 => false,
@@ -127,6 +182,13 @@ impl Protocol {
/// Whether or not the specified version uses the fee algorithm from Monero
/// hard fork version 16 (released in v18 binaries).
///
/// # Example
/// ```rust
/// # use monero_serai::{*, ringct::*};
/// assert_eq!(Protocol::v14.v16_fee(), false);
/// assert_eq!(Protocol::v16.v16_fee(), true);
/// ```
pub fn v16_fee(&self) -> bool {
match self {
Protocol::v14 => false,
@@ -188,11 +250,15 @@ impl Protocol {
}
}
/// Transparent structure representing a Pedersen commitment's contents.
/// Transparent structure representing a [https://web.getmonero.org/resources/moneropedia/pedersen-commitment.html]()'s contents.
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Commitment {
/// The value used to mask the `amount`.
pub mask: Scalar,
/// The value being masked.
///
/// In Monero's case, this is the amount of XMR in atomic units.
pub amount: u64,
}
@@ -208,6 +274,7 @@ impl Commitment {
Commitment { mask: Scalar::ONE, amount: 0 }
}
/// Create a new [`Self`].
pub fn new(mask: Scalar, amount: u64) -> Commitment {
Commitment { mask, amount }
}

View File

@@ -11,6 +11,7 @@ use monero_generators::hash_to_point;
use crate::{serialize::*, hash_to_scalar};
/// A signature within a [`RingSignature`].
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Signature {
c: Scalar,
@@ -18,23 +19,37 @@ pub struct Signature {
}
impl Signature {
/// Serialize [`Self`] into the writer `w`.
///
/// # Errors
/// This function returns any errors from the writer itself.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_scalar(&self.c, w)?;
write_scalar(&self.r, w)?;
Ok(())
}
/// Create [`Self`] from the reader `r`.
///
/// # Errors
/// This function returns an error if either the reader failed,
/// or if the data could not be deserialized into a [`Self`].
pub fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? })
}
}
/// A [ring signature](https://en.wikipedia.org/wiki/Ring_signature).
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct RingSignature {
sigs: Vec<Signature>,
}
impl RingSignature {
/// Serialize [`Self`] into the writer `w`.
///
/// # Errors
/// This function returns any errors from the writer itself.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
for sig in &self.sigs {
sig.write(w)?;
@@ -42,6 +57,11 @@ impl RingSignature {
Ok(())
}
/// Create [`Self`] from the reader `r`.
///
/// # Errors
/// This function returns an error if either the reader failed,
/// or if the data could not be deserialized into a [`Self`].
pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? })
}

View File

@@ -7,20 +7,21 @@ use monero_generators::H_pow_2;
use crate::{hash_to_scalar, unreduced_scalar::UnreducedScalar, serialize::*};
/// 64 Borromean ring signatures.
/// 64 Borromean ring signatures, as needed for a 64-bit range proof.
///
/// 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],
pub s1: [UnreducedScalar; 64],
pub ee: Scalar,
struct BorromeanSignatures {
s0: [UnreducedScalar; 64],
s1: [UnreducedScalar; 64],
ee: Scalar,
}
impl BorromeanSignatures {
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
/// Read a set of BorromeanSignatures from a reader.
fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
Ok(BorromeanSignatures {
s0: read_array(UnreducedScalar::read, r)?,
s1: read_array(UnreducedScalar::read, r)?,
@@ -28,7 +29,8 @@ impl BorromeanSignatures {
})
}
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
/// Write the set of BorromeanSignatures to a writer.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
for s0 in &self.s0 {
s0.write(w)?;
}
@@ -64,22 +66,26 @@ impl BorromeanSignatures {
/// A range proof premised on Borromean ring signatures.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct BorromeanRange {
pub sigs: BorromeanSignatures,
pub bit_commitments: [EdwardsPoint; 64],
sigs: BorromeanSignatures,
bit_commitments: [EdwardsPoint; 64],
}
impl BorromeanRange {
/// Read a BorromeanRange proof from a reader.
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanRange> {
Ok(BorromeanRange {
sigs: BorromeanSignatures::read(r)?,
bit_commitments: read_array(read_point, r)?,
})
}
/// Write the BorromeanRange proof to a reader.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.sigs.write(w)?;
write_raw_vec(write_point, &self.bit_commitments, w)
}
/// Verify the commitment contains a 64-bit value.
pub fn verify(&self, commitment: &EdwardsPoint) -> bool {
if &self.bit_commitments.iter().sum::<EdwardsPoint>() != commitment {
return false;

View File

@@ -26,15 +26,15 @@ use self::plus::*;
pub(crate) const MAX_OUTPUTS: usize = self::core::MAX_M;
/// Bulletproofs enum, supporting the original and plus formulations.
/// Bulletproof enum, encapsulating both Bulletproofs and Bulletproofs+.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Bulletproofs {
pub enum Bulletproof {
Original(OriginalStruct),
Plus(AggregateRangeProof),
}
impl Bulletproofs {
impl Bulletproof {
fn bp_fields(plus: bool) -> usize {
if plus {
6
@@ -57,7 +57,7 @@ impl Bulletproofs {
let mut bp_clawback = 0;
if n_padded_outputs > 2 {
let fields = Bulletproofs::bp_fields(plus);
let fields = Bulletproof::bp_fields(plus);
let base = ((fields + (2 * (LOG_N + 1))) * 32) / 2;
let size = (fields + (2 * LR_len)) * 32;
bp_clawback = ((base * n_padded_outputs) - size) * 4 / 5;
@@ -68,49 +68,51 @@ impl Bulletproofs {
pub(crate) fn fee_weight(plus: bool, outputs: usize) -> usize {
#[allow(non_snake_case)]
let (bp_clawback, LR_len) = Bulletproofs::calculate_bp_clawback(plus, outputs);
32 * (Bulletproofs::bp_fields(plus) + (2 * LR_len)) + 2 + bp_clawback
let (bp_clawback, LR_len) = Bulletproof::calculate_bp_clawback(plus, outputs);
32 * (Bulletproof::bp_fields(plus) + (2 * LR_len)) + 2 + bp_clawback
}
/// Prove the list of commitments are within [0 .. 2^64).
/// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof.
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
outputs: &[Commitment],
plus: bool,
) -> Result<Bulletproofs, TransactionError> {
) -> Result<Bulletproof, TransactionError> {
if outputs.is_empty() {
Err(TransactionError::NoOutputs)?;
}
if outputs.len() > MAX_OUTPUTS {
Err(TransactionError::TooManyOutputs)?;
}
Ok(if !plus {
Bulletproofs::Original(OriginalStruct::prove(rng, outputs))
} else {
use dalek_ff_group::EdwardsPoint as DfgPoint;
Bulletproofs::Plus(
AggregateRangeStatement::new(outputs.iter().map(|com| DfgPoint(com.calculate())).collect())
.unwrap()
.prove(rng, &Zeroizing::new(AggregateRangeWitness::new(outputs.to_vec()).unwrap()))
.unwrap(),
)
})
Ok(Bulletproof::Original(OriginalStruct::prove(rng, outputs)))
}
/// Verify the given Bulletproofs.
/// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof+.
pub fn prove_plus<R: RngCore + CryptoRng>(
rng: &mut R,
outputs: Vec<Commitment>,
) -> Result<Bulletproof, TransactionError> {
if outputs.is_empty() {
Err(TransactionError::NoOutputs)?;
}
if outputs.len() > MAX_OUTPUTS {
Err(TransactionError::TooManyOutputs)?;
}
Ok(Bulletproof::Plus(
AggregateRangeStatement::new(outputs.iter().map(Commitment::calculate).collect())
.unwrap()
.prove(rng, &Zeroizing::new(AggregateRangeWitness::new(outputs).unwrap()))
.unwrap(),
))
}
/// Verify the given Bulletproof(+).
#[must_use]
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
match self {
Bulletproofs::Original(bp) => bp.verify(rng, commitments),
Bulletproofs::Plus(bp) => {
Bulletproof::Original(bp) => bp.verify(rng, commitments),
Bulletproof::Plus(bp) => {
let mut verifier = BatchVerifier::new(1);
// If this commitment is torsioned (which is allowed), this won't be a well-formed
// dfg::EdwardsPoint (expected to be of prime-order)
// The actual BP+ impl will perform a torsion clear though, making this safe
// TODO: Have AggregateRangeStatement take in dalek EdwardsPoint for clarity on this
let Some(statement) = AggregateRangeStatement::new(
commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(),
) else {
let Some(statement) = AggregateRangeStatement::new(commitments.to_vec()) else {
return false;
};
if !statement.verify(rng, &mut verifier, (), bp.clone()) {
@@ -121,9 +123,11 @@ impl Bulletproofs {
}
}
/// Accumulate the verification for the given Bulletproofs into the specified BatchVerifier.
/// Returns false if the Bulletproofs aren't sane, without mutating the BatchVerifier.
/// Returns true if the Bulletproofs are sane, regardless of their validity.
/// Accumulate the verification for the given Bulletproof into the specified BatchVerifier.
///
/// Returns false if the Bulletproof isn't sane, leaving the BatchVerifier in an undefined
/// state.
/// Returns true if the Bulletproof is sane, regardless of their validity.
#[must_use]
pub fn batch_verify<ID: Copy + Zeroize, R: RngCore + CryptoRng>(
&self,
@@ -133,11 +137,9 @@ impl Bulletproofs {
commitments: &[EdwardsPoint],
) -> bool {
match self {
Bulletproofs::Original(bp) => bp.batch_verify(rng, verifier, id, commitments),
Bulletproofs::Plus(bp) => {
let Some(statement) = AggregateRangeStatement::new(
commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(),
) else {
Bulletproof::Original(bp) => bp.batch_verify(rng, verifier, id, commitments),
Bulletproof::Plus(bp) => {
let Some(statement) = AggregateRangeStatement::new(commitments.to_vec()) else {
return false;
};
statement.verify(rng, verifier, id, bp.clone())
@@ -151,7 +153,7 @@ impl Bulletproofs {
specific_write_vec: F,
) -> io::Result<()> {
match self {
Bulletproofs::Original(bp) => {
Bulletproof::Original(bp) => {
write_point(&bp.A, w)?;
write_point(&bp.S, w)?;
write_point(&bp.T1, w)?;
@@ -165,7 +167,7 @@ impl Bulletproofs {
write_scalar(&bp.t, w)
}
Bulletproofs::Plus(bp) => {
Bulletproof::Plus(bp) => {
write_point(&bp.A.0, w)?;
write_point(&bp.wip.A.0, w)?;
write_point(&bp.wip.B.0, w)?;
@@ -182,19 +184,21 @@ impl Bulletproofs {
self.write_core(w, |points, w| write_raw_vec(write_point, points, w))
}
/// Write the Bulletproof(+) to a writer.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.write_core(w, |points, w| write_vec(write_point, points, w))
}
/// Serialize the Bulletproof(+) to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
/// Read Bulletproofs.
pub fn read<R: Read>(r: &mut R) -> io::Result<Bulletproofs> {
Ok(Bulletproofs::Original(OriginalStruct {
/// Read a Bulletproof.
pub fn read<R: Read>(r: &mut R) -> io::Result<Bulletproof> {
Ok(Bulletproof::Original(OriginalStruct {
A: read_point(r)?,
S: read_point(r)?,
T1: read_point(r)?,
@@ -209,11 +213,11 @@ impl Bulletproofs {
}))
}
/// Read Bulletproofs+.
pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproofs> {
/// Read a Bulletproof+.
pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproof> {
use dalek_ff_group::{Scalar as DfgScalar, EdwardsPoint as DfgPoint};
Ok(Bulletproofs::Plus(AggregateRangeProof {
Ok(Bulletproof::Plus(AggregateRangeProof {
A: DfgPoint(read_point(r)?),
wip: WipProof {
A: DfgPoint(read_point(r)?),

View File

@@ -33,6 +33,7 @@ pub(crate) fn hadamard_fold(
res
}
/// Internal structure representing a Bulletproof.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct OriginalStruct {
pub(crate) A: DalekPoint,

View File

@@ -9,6 +9,7 @@ use group::{
ff::{Field, PrimeField},
Group, GroupEncoding,
};
use curve25519_dalek::EdwardsPoint as DalekPoint;
use dalek_ff_group::{Scalar, EdwardsPoint};
use crate::{
@@ -28,7 +29,7 @@ use crate::{
#[derive(Clone, Debug)]
pub(crate) struct AggregateRangeStatement {
generators: Generators,
V: Vec<EdwardsPoint>,
V: Vec<DalekPoint>,
}
impl Zeroize for AggregateRangeStatement {
@@ -50,6 +51,7 @@ impl AggregateRangeWitness {
}
}
/// Internal structure representing a Bulletproof+, as used in Monero.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct AggregateRangeProof {
pub(crate) A: EdwardsPoint,
@@ -57,7 +59,7 @@ pub struct AggregateRangeProof {
}
impl AggregateRangeStatement {
pub(crate) fn new(V: Vec<EdwardsPoint>) -> Option<Self> {
pub(crate) fn new(V: Vec<DalekPoint>) -> Option<Self> {
if V.is_empty() || (V.len() > MAX_M) {
return None;
}
@@ -98,11 +100,14 @@ impl AggregateRangeStatement {
}
let mn = V.len() * N;
// 2, 4, 6, 8... powers of z, of length equivalent to the amount of commitments
let mut z_pow = Vec::with_capacity(V.len());
// z**2
z_pow.push(z * z);
let mut d = ScalarVector::new(mn);
for j in 1 ..= V.len() {
z_pow.push(z.pow(Scalar::from(2 * u64::try_from(j).unwrap()))); // TODO: Optimize this
z_pow.push(*z_pow.last().unwrap() * z_pow[0]);
d = d + &(Self::d_j(j, V.len()) * (z_pow[j - 1]));
}
@@ -157,7 +162,7 @@ impl AggregateRangeStatement {
return None;
}
for (commitment, witness) in self.V.iter().zip(witness.0.iter()) {
if witness.calculate() != **commitment {
if witness.calculate() != *commitment {
return None;
}
}
@@ -170,9 +175,9 @@ impl AggregateRangeStatement {
// Commitments aren't transmitted INV_EIGHT though, so this multiplies by INV_EIGHT to enable
// clearing its cofactor without mutating the value
// For some reason, these values are transcripted * INV_EIGHT, not as transmitted
let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::<Vec<_>>();
let V = V.into_iter().map(|V| V * crate::INV_EIGHT()).collect::<Vec<_>>();
let mut transcript = initial_transcript(V.iter());
V.iter_mut().for_each(|V| *V = V.mul_by_cofactor());
let mut V = V.into_iter().map(|V| EdwardsPoint(V.mul_by_cofactor())).collect::<Vec<_>>();
// Pad V
while V.len() < padded_pow_of_2(V.len()) {
@@ -239,9 +244,11 @@ impl AggregateRangeStatement {
) -> bool {
let Self { generators, V } = self;
let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::<Vec<_>>();
let V = V.into_iter().map(|V| V * crate::INV_EIGHT()).collect::<Vec<_>>();
let mut transcript = initial_transcript(V.iter());
V.iter_mut().for_each(|V| *V = V.mul_by_cofactor());
// With the torsion clear, wrap it into a EdwardsPoint from dalek-ff-group
// (which is prime-order)
let V = V.into_iter().map(|V| EdwardsPoint(V.mul_by_cofactor())).collect::<Vec<_>>();
let generators = generators.reduce(V.len() * N);

View File

@@ -1,6 +1,7 @@
use std_shims::{sync::OnceLock, vec::Vec};
use dalek_ff_group::{Scalar, EdwardsPoint};
use curve25519_dalek::EdwardsPoint;
use dalek_ff_group::Scalar;
use monero_generators::{hash_to_point as raw_hash_to_point};
use crate::{hash, hash_to_scalar as dalek_hash};

View File

@@ -26,31 +26,36 @@ use crate::{
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
pub(crate) use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
/// Errors returned when CLSAG signing fails.
/// Errors when working with CLSAGs.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum ClsagError {
#[cfg_attr(feature = "std", error("internal error ({0})"))]
InternalError(&'static str),
/// The ring was invalid (such as being too small or too large).
#[cfg_attr(feature = "std", error("invalid ring"))]
InvalidRing,
/// The specified ring member was invalid (index, ring size).
#[cfg_attr(feature = "std", error("invalid ring member (member {0}, ring size {1})"))]
InvalidRingMember(u8, u8),
/// The commitment opening provided did not match the ring member's.
#[cfg_attr(feature = "std", error("invalid commitment"))]
InvalidCommitment,
/// The key image was invalid (such as being identity or torsioned)
#[cfg_attr(feature = "std", error("invalid key image"))]
InvalidImage,
/// The `D` component was invalid.
#[cfg_attr(feature = "std", error("invalid D"))]
InvalidD,
/// The `s` vector was invalid.
#[cfg_attr(feature = "std", error("invalid s"))]
InvalidS,
/// The `c1` variable was invalid.
#[cfg_attr(feature = "std", error("invalid c1"))]
InvalidC1,
}
/// Input being signed for.
/// Context on the ring member being signed for.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ClsagInput {
// The actual commitment for the true spend
@@ -63,7 +68,7 @@ impl ClsagInput {
pub fn new(commitment: Commitment, decoys: Decoys) -> Result<ClsagInput, ClsagError> {
let n = decoys.len();
if n > u8::MAX.into() {
Err(ClsagError::InternalError("max ring size in this library is u8 max"))?;
Err(ClsagError::InvalidRing)?;
}
let n = u8::try_from(n).unwrap();
if decoys.i >= n {
@@ -213,9 +218,16 @@ fn core(
/// CLSAG signature, as used in Monero.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Clsag {
pub D: EdwardsPoint,
pub s: Vec<Scalar>,
pub c1: Scalar,
D: EdwardsPoint,
pub(crate) s: Vec<Scalar>,
pub(crate) c1: Scalar,
}
pub(crate) struct ClsagSignCore {
incomplete_clsag: Clsag,
pseudo_out: EdwardsPoint,
key_challenge: Scalar,
challenged_mask: Scalar,
}
impl Clsag {
@@ -229,28 +241,34 @@ impl Clsag {
msg: &[u8; 32],
A: EdwardsPoint,
AH: EdwardsPoint,
) -> (Clsag, EdwardsPoint, Scalar, Scalar) {
) -> ClsagSignCore {
let r: usize = input.decoys.i.into();
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
let z = input.commitment.mask - mask;
let mask_delta = input.commitment.mask - mask;
let H = hash_to_point(&input.decoys.ring[r][0]);
let D = H * z;
let D = H * mask_delta;
let mut s = Vec::with_capacity(input.decoys.ring.len());
for _ in 0 .. input.decoys.ring.len() {
s.push(random_scalar(rng));
}
let ((D, p, c), c1) =
let ((D, c_p, c_c), c1) =
core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, &Mode::Sign(r, A, AH));
(Clsag { D, s, c1 }, pseudo_out, p, c * z)
ClsagSignCore {
incomplete_clsag: Clsag { D, s, c1 },
pseudo_out,
key_challenge: c_p,
challenged_mask: c_c * mask_delta,
}
}
/// Generate CLSAG signatures for the given inputs.
///
/// inputs is of the form (private key, key image, input).
/// sum_outputs is for the sum of the outputs' commitment masks.
pub fn sign<R: RngCore + CryptoRng>(
pub(crate) fn sign<R: RngCore + CryptoRng>(
rng: &mut R,
mut inputs: Vec<(Zeroizing<Scalar>, EdwardsPoint, ClsagInput)>,
sum_outputs: Scalar,
@@ -267,7 +285,8 @@ impl Clsag {
}
let mut nonce = Zeroizing::new(random_scalar(rng));
let (mut clsag, pseudo_out, p, c) = Clsag::sign_core(
let ClsagSignCore { mut incomplete_clsag, pseudo_out, key_challenge, challenged_mask } =
Clsag::sign_core(
rng,
&inputs[i].1,
&inputs[i].2,
@@ -280,7 +299,11 @@ impl Clsag {
// Effectively r - cx, except cx is (c_p x) + (c_c z), where z is the delta between a ring
// member's commitment and our input commitment (which will only have a known discrete log
// over G if the amounts cancel out)
clsag.s[usize::from(inputs[i].2.decoys.i)] = nonce.deref() - ((p * inputs[i].0.deref()) + c);
incomplete_clsag.s[usize::from(inputs[i].2.decoys.i)] =
nonce.deref() - ((key_challenge * inputs[i].0.deref()) + challenged_mask);
let clsag = incomplete_clsag;
// Zeroize private keys and nonces.
inputs[i].0.zeroize();
nonce.zeroize();
@@ -310,7 +333,7 @@ impl Clsag {
if ring.len() != self.s.len() {
Err(ClsagError::InvalidS)?;
}
if I.is_identity() {
if I.is_identity() || (!I.is_torsion_free()) {
Err(ClsagError::InvalidImage)?;
}
@@ -330,12 +353,14 @@ impl Clsag {
(ring_len * 32) + 32 + 32
}
/// Write the CLSAG to a writer.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_raw_vec(write_scalar, &self.s, w)?;
w.write_all(&self.c1.to_bytes())?;
write_point(&self.D, w)
}
/// Read a CLSAG from a reader.
pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Clsag> {
Ok(Clsag { s: read_raw_vec(read_scalar, decoys, r)?, c1: read_scalar(r)?, D: read_point(r)? })
}

View File

@@ -57,13 +57,13 @@ impl ClsagInput {
/// CLSAG input and the mask to use for it.
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ClsagDetails {
pub(crate) struct ClsagDetails {
input: ClsagInput,
mask: Scalar,
}
impl ClsagDetails {
pub fn new(input: ClsagInput, mask: Scalar) -> ClsagDetails {
pub(crate) fn new(input: ClsagInput, mask: Scalar) -> ClsagDetails {
ClsagDetails { input, mask }
}
}
@@ -93,7 +93,7 @@ struct Interim {
/// FROST algorithm for producing a CLSAG signature.
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub struct ClsagMultisig {
pub(crate) struct ClsagMultisig {
transcript: RecommendedTranscript,
pub(crate) H: EdwardsPoint,
@@ -107,7 +107,7 @@ pub struct ClsagMultisig {
}
impl ClsagMultisig {
pub fn new(
pub(crate) fn new(
transcript: RecommendedTranscript,
output_key: EdwardsPoint,
details: Arc<RwLock<Option<ClsagDetails>>>,
@@ -219,8 +219,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes"));
#[allow(non_snake_case)]
let (clsag, pseudo_out, p, c) = Clsag::sign_core(
let sign_core = Clsag::sign_core(
&mut rng,
&self.image.expect("verifying a share despite never processing any addendums").0,
&self.input(),
@@ -229,10 +228,15 @@ impl Algorithm<Ed25519> for ClsagMultisig {
nonce_sums[0][0].0,
nonce_sums[0][1].0,
);
self.interim = Some(Interim { p, c, clsag, pseudo_out });
self.interim = Some(Interim {
p: sign_core.key_challenge,
c: sign_core.challenged_mask,
clsag: sign_core.incomplete_clsag,
pseudo_out: sign_core.pseudo_out,
});
// r - p x, where p is the challenge for the keys
*nonces[0] - dfg::Scalar(p) * view.secret_share().deref()
*nonces[0] - dfg::Scalar(sign_core.key_challenge) * view.secret_share().deref()
}
#[must_use]

View File

@@ -11,28 +11,36 @@ use monero_generators::H;
use crate::{hash_to_scalar, ringct::hash_to_point, serialize::*};
/// Errors when working with MLSAGs.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum MlsagError {
/// Invalid ring (such as too small or too large).
#[cfg_attr(feature = "std", error("invalid ring"))]
InvalidRing,
/// Invalid amount of key images.
#[cfg_attr(feature = "std", error("invalid amount of key images"))]
InvalidAmountOfKeyImages,
/// Invalid ss matrix.
#[cfg_attr(feature = "std", error("invalid ss"))]
InvalidSs,
#[cfg_attr(feature = "std", error("key image was identity"))]
IdentityKeyImage,
/// Invalid key image.
#[cfg_attr(feature = "std", error("invalid key image"))]
InvalidKeyImage,
/// Invalid ci vector.
#[cfg_attr(feature = "std", error("invalid ci"))]
InvalidCi,
}
/// A vector of rings, forming a matrix, to verify the MLSAG with.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct RingMatrix {
matrix: Vec<Vec<EdwardsPoint>>,
}
impl RingMatrix {
pub fn new(matrix: Vec<Vec<EdwardsPoint>>) -> Result<Self, MlsagError> {
/// Construct a ring matrix from an already formatted series of points.
fn new(matrix: Vec<Vec<EdwardsPoint>>) -> Result<Self, MlsagError> {
// Monero requires that there is more than one ring member for MLSAG signatures:
// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
// src/ringct/rctSigs.cpp#L462
@@ -60,11 +68,12 @@ impl RingMatrix {
RingMatrix::new(matrix)
}
pub fn iter(&self) -> impl Iterator<Item = &[EdwardsPoint]> {
/// Iterate the members of the matrix.
fn iter(&self) -> impl Iterator<Item = &[EdwardsPoint]> {
self.matrix.iter().map(AsRef::as_ref)
}
/// Return the amount of members in the ring.
/// Returns the amount of members in the ring.
pub fn members(&self) -> usize {
self.matrix.len()
}
@@ -79,13 +88,15 @@ impl RingMatrix {
}
}
/// The MLSAG linkable ring signature, as used in Monero.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Mlsag {
pub ss: Vec<Vec<Scalar>>,
pub cc: Scalar,
ss: Vec<Vec<Scalar>>,
cc: Scalar,
}
impl Mlsag {
/// Write the MLSAG to a writer.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
for ss in &self.ss {
write_raw_vec(write_scalar, ss, w)?;
@@ -93,6 +104,7 @@ impl Mlsag {
write_scalar(&self.cc, w)
}
/// Read the MLSAG from a reader.
pub fn read<R: Read>(mixins: usize, ss_2_elements: usize, r: &mut R) -> io::Result<Mlsag> {
Ok(Mlsag {
ss: (0 .. mixins)
@@ -102,6 +114,7 @@ impl Mlsag {
})
}
/// Verify the MLSAG.
pub fn verify(
&self,
msg: &[u8; 32],
@@ -142,8 +155,8 @@ 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)?;
if ki.is_identity() || (!ki.is_torsion_free()) {
Err(MlsagError::InvalidKeyImage)?;
}
#[allow(non_snake_case)]
@@ -164,8 +177,9 @@ impl Mlsag {
}
}
/// An aggregate ring matrix builder, usable to set up the ring matrix to prove/verify an aggregate
/// MLSAG signature.
/// Builder for a RingMatrix when using an aggregate signature.
///
/// This handles the formatting as necessary.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct AggregateRingMatrixBuilder {
key_ring: Vec<Vec<EdwardsPoint>>,
@@ -206,7 +220,7 @@ impl AggregateRingMatrixBuilder {
Ok(())
}
/// Build and return the [`RingMatrix`]
/// Build and return the [`RingMatrix`].
pub fn build(mut self) -> Result<RingMatrix, MlsagError> {
for (i, amount_commitment) in self.amounts_ring.drain(..).enumerate() {
self.key_ring[i].push(amount_commitment);

View File

@@ -23,7 +23,7 @@ pub mod bulletproofs;
use crate::{
Protocol,
serialize::*,
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs},
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof},
};
/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`.
@@ -31,6 +31,7 @@ pub fn generate_key_image(secret: &Zeroizing<Scalar>) -> EdwardsPoint {
hash_to_point(&(ED25519_BASEPOINT_TABLE * secret.deref())) * secret.deref()
}
/// An encrypted amount.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum EncryptedAmount {
Original { mask: [u8; 32], amount: [u8; 32] },
@@ -38,6 +39,7 @@ pub enum EncryptedAmount {
}
impl EncryptedAmount {
/// Read an EncryptedAmount from a reader.
pub fn read<R: Read>(compact: bool, r: &mut R) -> io::Result<EncryptedAmount> {
Ok(if !compact {
EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? }
@@ -46,6 +48,7 @@ impl EncryptedAmount {
})
}
/// Write the EncryptedAmount to a writer.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
EncryptedAmount::Original { mask, amount } => {
@@ -57,6 +60,7 @@ impl EncryptedAmount {
}
}
/// The type of the RingCT data.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum RctType {
/// No RCT proofs.
@@ -77,6 +81,18 @@ pub enum RctType {
}
impl RctType {
/// Convert [`self`] to its byte representation.
///
/// ```rust
/// # use monero_serai::ringct::*;
/// assert_eq!(RctType::Null.to_bytes(), 0);
/// assert_eq!(RctType::MlsagAggregate.to_bytes(), 1);
/// assert_eq!(RctType::MlsagIndividual.to_bytes(), 2);
/// assert_eq!(RctType::Bulletproofs.to_bytes(), 3);
/// assert_eq!(RctType::BulletproofsCompactAmount.to_bytes(), 4);
/// assert_eq!(RctType::Clsag.to_bytes(), 5);
/// assert_eq!(RctType::BulletproofsPlus.to_bytes(), 6);
/// ```
pub fn to_byte(self) -> u8 {
match self {
RctType::Null => 0,
@@ -89,6 +105,25 @@ impl RctType {
}
}
/// Create [`Self`] from a byte representation.
///
/// ```rust
/// # use monero_serai::ringct::*;
/// assert_eq!(RctType::from_bytes(0).unwrap(), RctType::Null);
/// assert_eq!(RctType::from_bytes(1).unwrap(), RctType::MlsagAggregate);
/// assert_eq!(RctType::from_bytes(2).unwrap(), RctType::MlsagIndividual);
/// assert_eq!(RctType::from_bytes(3).unwrap(), RctType::Bulletproofs);
/// assert_eq!(RctType::from_bytes(4).unwrap(), RctType::BulletproofsCompactAmount);
/// assert_eq!(RctType::from_bytes(5).unwrap(), RctType::Clsag);
/// assert_eq!(RctType::from_bytes(6).unwrap(), RctType::BulletproofsPlus);
/// ```
///
/// # Errors
/// This function returns [`None`] if the byte representation is invalid.
/// ```rust
/// # use monero_serai::ringct::*;
/// assert_eq!(RctType::from_bytes(7), None);
/// ```
pub fn from_byte(byte: u8) -> Option<Self> {
Some(match byte {
0 => RctType::Null,
@@ -102,22 +137,45 @@ impl RctType {
})
}
/// Returns true if this RctType uses compact encrypted amounts, false otherwise.
///
/// ```rust
/// # use monero_serai::ringct::*;
/// assert_eq!(RctType::Null.compact_encrypted_amounts(), false);
/// assert_eq!(RctType::MlsagAggregate.compact_encrypted_amounts(), false);
/// assert_eq!(RctType::MlsagIndividual.compact_encrypted_amounts(), false);
/// assert_eq!(RctType::Bulletproofs.compact_encrypted_amounts(), false);
/// assert_eq!(RctType::BulletproofsCompactAmount.compact_encrypted_amounts(), true);
/// assert_eq!(RctType::Clsag.compact_encrypted_amounts(), true);
/// assert_eq!(RctType::BulletproofsPlus.compact_encrypted_amounts(), true);
/// ```
pub fn compact_encrypted_amounts(&self) -> bool {
match self {
RctType::Null |
RctType::MlsagAggregate |
RctType::MlsagIndividual |
RctType::Bulletproofs => false,
RctType::Null
| RctType::MlsagAggregate
| RctType::MlsagIndividual
| RctType::Bulletproofs => false,
RctType::BulletproofsCompactAmount | RctType::Clsag | RctType::BulletproofsPlus => true,
}
}
}
/// The base of the RingCT data.
///
/// This excludes all proofs (which once initially verified do not need to be kept around) and
/// solely keeps data which either impacts the effects of the transactions or is needed to scan it.
///
/// The one exception for this is `pseudo_outs`, which was originally present here yet moved to
/// RctPrunable in a later hard fork (causing it to be present in both).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RctBase {
/// The fee used by this transaction.
pub fee: u64,
/// The re-randomized amount commitments used within inputs.
pub pseudo_outs: Vec<EdwardsPoint>,
/// The encrypted amounts for the recipient to decrypt.
pub encrypted_amounts: Vec<EncryptedAmount>,
/// The output commitments.
pub commitments: Vec<EdwardsPoint>,
}
@@ -127,6 +185,7 @@ impl RctBase {
1 + (outputs * (8 + 32)) + varint_len(fee)
}
/// Write the RctBase to a writer.
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
w.write_all(&[rct_type.to_byte()])?;
match rct_type {
@@ -144,16 +203,17 @@ impl RctBase {
}
}
/// Read a RctBase from a writer.
pub fn read<R: Read>(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> {
let rct_type =
RctType::from_byte(read_byte(r)?).ok_or_else(|| io::Error::other("invalid RCT type"))?;
match rct_type {
RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {}
RctType::Bulletproofs |
RctType::BulletproofsCompactAmount |
RctType::Clsag |
RctType::BulletproofsPlus => {
RctType::Bulletproofs
| RctType::BulletproofsCompactAmount
| RctType::Clsag
| RctType::BulletproofsPlus => {
if outputs == 0 {
// Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if
// Bulletproofs are in use
@@ -171,12 +231,14 @@ impl RctBase {
} else {
RctBase {
fee: read_varint(r)?,
// Only read pseudo_outs if they have yet to be moved to RctPrunable
// TODO: Shouldn't this be any Mlsag*?
pseudo_outs: if rct_type == RctType::MlsagIndividual {
read_raw_vec(read_point, inputs, r)?
} else {
vec![]
},
encrypted_amounts: (0 .. outputs)
encrypted_amounts: (0..outputs)
.map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r))
.collect::<Result<_, _>>()?,
commitments: read_raw_vec(read_point, outputs, r)?,
@@ -187,6 +249,7 @@ impl RctBase {
}
}
/// The prunable portion of the RingCT data.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum RctPrunable {
Null,
@@ -199,12 +262,12 @@ pub enum RctPrunable {
mlsags: Vec<Mlsag>,
},
MlsagBulletproofs {
bulletproofs: Bulletproofs,
bulletproofs: Bulletproof,
mlsags: Vec<Mlsag>,
pseudo_outs: Vec<EdwardsPoint>,
},
Clsag {
bulletproofs: Bulletproofs,
bulletproofs: Bulletproof,
clsags: Vec<Clsag>,
pseudo_outs: Vec<EdwardsPoint>,
},
@@ -213,10 +276,14 @@ pub enum RctPrunable {
impl RctPrunable {
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize {
// 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one)
1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) +
(inputs * (Clsag::fee_weight(protocol.ring_len()) + 32))
1 + Bulletproof::fee_weight(protocol.bp_plus(), outputs)
+ (inputs * (Clsag::fee_weight(protocol.ring_len()) + 32))
}
/// Serialize [`Self`] into the writer `w`.
///
/// # Errors
/// This function returns any errors from the writer itself.
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
match self {
RctPrunable::Null => Ok(()),
@@ -249,12 +316,18 @@ impl RctPrunable {
}
}
/// Serialize [`Self`] into a new byte buffer.
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized, rct_type).unwrap();
serialized
}
/// Create [`Self`] from the reader `r`.
///
/// # Errors
/// This function returns an error if either the reader failed,
/// or if the data could not be deserialized into a [`Self`].
pub fn read<R: Read>(
rct_type: RctType,
ring_length: usize,
@@ -281,7 +354,7 @@ impl RctPrunable {
},
RctType::MlsagIndividual => RctPrunable::MlsagBorromean {
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
mlsags: (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?,
mlsags: (0..inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?,
},
RctType::Bulletproofs | RctType::BulletproofsCompactAmount => {
RctPrunable::MlsagBulletproofs {
@@ -294,11 +367,9 @@ impl RctPrunable {
{
Err(io::Error::other("n bulletproofs instead of one"))?;
}
Bulletproofs::read(r)?
Bulletproof::read(r)?
},
mlsags: (0 .. inputs)
.map(|_| Mlsag::read(ring_length, 2, r))
.collect::<Result<_, _>>()?,
mlsags: (0..inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?,
pseudo_outs: read_raw_vec(read_point, inputs, r)?,
}
}
@@ -307,11 +378,9 @@ impl RctPrunable {
if read_varint::<_, u64>(r)? != 1 {
Err(io::Error::other("n bulletproofs instead of one"))?;
}
(if rct_type == RctType::Clsag { Bulletproofs::read } else { Bulletproofs::read_plus })(
r,
)?
(if rct_type == RctType::Clsag { Bulletproof::read } else { Bulletproof::read_plus })(r)?
},
clsags: (0 .. inputs).map(|_| Clsag::read(ring_length, r)).collect::<Result<_, _>>()?,
clsags: (0..inputs).map(|_| Clsag::read(ring_length, r)).collect::<Result<_, _>>()?,
pseudo_outs: read_raw_vec(read_point, inputs, r)?,
},
})
@@ -320,19 +389,22 @@ impl RctPrunable {
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"),
RctPrunable::AggregateMlsagBorromean { borromean, .. } |
RctPrunable::MlsagBorromean { borromean, .. } => {
RctPrunable::AggregateMlsagBorromean { borromean, .. }
| RctPrunable::MlsagBorromean { borromean, .. } => {
borromean.iter().try_for_each(|rs| rs.write(w))
}
RctPrunable::MlsagBulletproofs { bulletproofs, .. } |
RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w),
RctPrunable::MlsagBulletproofs { bulletproofs, .. }
| RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w),
}
}
}
/// RingCT signature data.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RctSignatures {
/// The base of the RingCT data.
pub base: RctBase,
/// The prunable portion of the RingCT data.
pub prunable: RctPrunable,
}
@@ -360,7 +432,7 @@ impl RctSignatures {
}
}
RctPrunable::Clsag { bulletproofs, .. } => {
if matches!(bulletproofs, Bulletproofs::Original { .. }) {
if matches!(bulletproofs, Bulletproof::Original { .. }) {
RctType::Clsag
} else {
RctType::BulletproofsPlus
@@ -373,18 +445,28 @@ impl RctSignatures {
RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(protocol, inputs, outputs)
}
/// Serialize [`Self`] into the writer `w`.
///
/// # Errors
/// This function returns any errors from the writer itself.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
let rct_type = self.rct_type();
self.base.write(w, rct_type)?;
self.prunable.write(w, rct_type)
}
/// Serialize [`Self`] into a new byte buffer.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
/// Create [`Self`] from the reader `r` and other data.
///
/// # Errors
/// This function returns an error if either the reader failed,
/// or if the data could not be deserialized into a [`Self`].
pub fn read<R: Read>(
ring_length: usize,
inputs: usize,

View File

@@ -7,7 +7,8 @@ use multiexp::BatchVerifier;
use crate::{
Commitment, random_scalar,
ringct::bulletproofs::{Bulletproofs, original::OriginalStruct},
ringct::bulletproofs::{Bulletproof, original::OriginalStruct},
wallet::TransactionError,
};
mod plus;
@@ -18,7 +19,7 @@ fn bulletproofs_vector() {
let point = |point| decompress_point(point).unwrap();
// Generated from Monero
assert!(Bulletproofs::Original(OriginalStruct {
assert!(Bulletproof::Original(OriginalStruct {
A: point(hex!("ef32c0b9551b804decdcb107eb22aa715b7ce259bf3c5cac20e24dfa6b28ac71")),
S: point(hex!("e1285960861783574ee2b689ae53622834eb0b035d6943103f960cd23e063fa0")),
T1: point(hex!("4ea07735f184ba159d0e0eb662bac8cde3eb7d39f31e567b0fbda3aa23fe5620")),
@@ -70,7 +71,11 @@ macro_rules! bulletproofs_tests {
.map(|i| Commitment::new(random_scalar(&mut OsRng), u64::try_from(i).unwrap()))
.collect::<Vec<_>>();
let bp = Bulletproofs::prove(&mut OsRng, &commitments, $plus).unwrap();
let bp = if $plus {
Bulletproof::prove_plus(&mut OsRng, commitments.clone()).unwrap()
} else {
Bulletproof::prove(&mut OsRng, &commitments).unwrap()
};
let commitments = commitments.iter().map(Commitment::calculate).collect::<Vec<_>>();
assert!(bp.verify(&mut OsRng, &commitments));
@@ -86,7 +91,15 @@ macro_rules! bulletproofs_tests {
for _ in 0 .. 17 {
commitments.push(Commitment::new(Scalar::ZERO, 0));
}
assert!(Bulletproofs::prove(&mut OsRng, &commitments, $plus).is_err());
assert_eq!(
(if $plus {
Bulletproof::prove_plus(&mut OsRng, commitments)
} else {
Bulletproof::prove(&mut OsRng, &commitments)
})
.unwrap_err(),
TransactionError::TooManyOutputs,
);
}
};
}

View File

@@ -2,7 +2,7 @@ use rand_core::{RngCore, OsRng};
use multiexp::BatchVerifier;
use group::ff::Field;
use dalek_ff_group::{Scalar, EdwardsPoint};
use dalek_ff_group::Scalar;
use crate::{
Commitment,
@@ -19,7 +19,7 @@ fn test_aggregate_range_proof() {
for _ in 0 .. m {
commitments.push(Commitment::new(*Scalar::random(&mut OsRng), OsRng.next_u64()));
}
let commitment_points = commitments.iter().map(|com| EdwardsPoint(com.calculate())).collect();
let commitment_points = commitments.iter().map(Commitment::calculate).collect();
let statement = AggregateRangeStatement::new(commitment_points).unwrap();
let witness = AggregateRangeWitness::new(commitments).unwrap();

View File

@@ -12,7 +12,7 @@ use crate::{
Protocol, hash,
serialize::*,
ring_signatures::RingSignature,
ringct::{bulletproofs::Bulletproofs, RctType, RctBase, RctPrunable, RctSignatures},
ringct::{bulletproofs::Bulletproof, RctType, RctBase, RctPrunable, RctSignatures},
};
#[derive(Clone, PartialEq, Eq, Debug)]
@@ -426,7 +426,7 @@ impl Transaction {
if !(bp || bp_plus) {
blob_size
} else {
blob_size + Bulletproofs::calculate_bp_clawback(bp_plus, self.prefix.outputs.len()).0
blob_size + Bulletproof::calculate_bp_clawback(bp_plus, self.prefix.outputs.len()).0
}
}
}

View File

@@ -34,10 +34,6 @@ impl UnreducedScalar {
Ok(UnreducedScalar(read_bytes(r)?))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
fn as_bits(&self) -> [u8; 256] {
let mut bits = [0; 256];
for (i, bit) in bits.iter_mut().enumerate() {

View File

@@ -1,13 +1,5 @@
use std_shims::{vec::Vec, collections::HashSet};
#[cfg(feature = "cache-distribution")]
use std_shims::sync::OnceLock;
#[cfg(all(feature = "cache-distribution", not(feature = "std")))]
use std_shims::sync::Mutex;
#[cfg(all(feature = "cache-distribution", feature = "std"))]
use async_lock::Mutex;
use zeroize::{Zeroize, ZeroizeOnDrop};
use rand_core::{RngCore, CryptoRng};
@@ -29,16 +21,6 @@ const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
#[allow(clippy::cast_precision_loss)]
const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;
// TODO: Resolve safety of this in case a reorg occurs/the network changes
// TODO: Update this when scanning a block, as possible
#[cfg(feature = "cache-distribution")]
static DISTRIBUTION_CELL: OnceLock<Mutex<Vec<u64>>> = OnceLock::new();
#[cfg(feature = "cache-distribution")]
#[allow(non_snake_case)]
fn DISTRIBUTION() -> &'static Mutex<Vec<u64>> {
DISTRIBUTION_CELL.get_or_init(|| Mutex::new(Vec::with_capacity(3000000)))
}
#[allow(clippy::too_many_arguments)]
async fn select_n<'a, R: RngCore + CryptoRng, RPC: RpcConnection>(
rng: &mut R,
@@ -158,14 +140,6 @@ async fn select_decoys<R: RngCore + CryptoRng, RPC: RpcConnection>(
inputs: &[SpendableOutput],
fingerprintable_canonical: bool,
) -> Result<Vec<Decoys>, RpcError> {
#[cfg(feature = "cache-distribution")]
#[cfg(not(feature = "std"))]
let mut distribution = DISTRIBUTION().lock();
#[cfg(feature = "cache-distribution")]
#[cfg(feature = "std")]
let mut distribution = DISTRIBUTION().lock().await;
#[cfg(not(feature = "cache-distribution"))]
let mut distribution = vec![];
let decoy_count = ring_len - 1;

View File

@@ -10,7 +10,7 @@ use curve25519_dalek::{
};
use crate::{
hash, hash_to_scalar, serialize::write_varint, ringct::EncryptedAmount, transaction::Input,
hash, hash_to_scalar, serialize::write_varint, Commitment, ringct::EncryptedAmount, transaction::Input,
};
pub mod extra;
@@ -94,18 +94,21 @@ pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
hash_to_scalar(&mask)
}
pub(crate) fn amount_encryption(amount: u64, key: Scalar) -> [u8; 8] {
pub(crate) fn compact_amount_encryption(amount: u64, key: Scalar) -> [u8; 8] {
let mut amount_mask = b"amount".to_vec();
amount_mask.extend(key.to_bytes());
(amount ^ u64::from_le_bytes(hash(&amount_mask)[.. 8].try_into().unwrap())).to_le_bytes()
}
// TODO: Move this under EncryptedAmount?
fn amount_decryption(amount: &EncryptedAmount, key: Scalar) -> (Scalar, u64) {
match amount {
impl EncryptedAmount {
/// Decrypt an EncryptedAmount into the Commitment it encrypts.
///
/// The caller must verify the decrypted Commitment matches with the actual Commitment used
/// within in the Monero protocol.
pub fn decrypt(&self, key: Scalar) -> Commitment {
match self {
// TODO: Add a test vector for this
EncryptedAmount::Original { mask, amount } => {
#[cfg(feature = "experimental")]
{
let mask_shared_sec = hash(key.as_bytes());
let mask =
Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec);
@@ -116,21 +119,14 @@ fn amount_decryption(amount: &EncryptedAmount, key: Scalar) -> (Scalar, u64) {
// d2b from rctTypes.cpp
let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap());
(mask, amount)
Commitment::new(mask, amount)
}
#[cfg(not(feature = "experimental"))]
{
let _ = mask;
let _ = amount;
todo!("decrypting a legacy monero transaction's amount")
}
}
EncryptedAmount::Compact { amount } => (
EncryptedAmount::Compact { amount } => Commitment::new(
commitment_mask(key),
u64::from_le_bytes(amount_encryption(u64::from_le_bytes(*amount), key)),
u64::from_le_bytes(compact_amount_encryption(u64::from_le_bytes(*amount), key)),
),
}
}
}
/// The private view key and public spend key, enabling scanning transactions.

View File

@@ -17,9 +17,7 @@ use crate::{
transaction::{Input, Timelock, Transaction},
block::Block,
rpc::{RpcError, RpcConnection, Rpc},
wallet::{
PaymentId, Extra, address::SubaddressIndex, Scanner, uniqueness, shared_key, amount_decryption,
},
wallet::{PaymentId, Extra, address::SubaddressIndex, Scanner, uniqueness, shared_key},
};
/// An absolute output ID, defined as its transaction hash and output index.
@@ -427,15 +425,13 @@ impl Scanner {
commitment.amount = amount;
// Regular transaction
} else {
let (mask, amount) = match tx.rct_signatures.base.encrypted_amounts.get(o) {
Some(amount) => amount_decryption(amount, shared_key),
commitment = match tx.rct_signatures.base.encrypted_amounts.get(o) {
Some(amount) => amount.decrypt(shared_key),
// This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case
None => break,
};
// Rebuild the commitment to verify it
commitment = Commitment::new(mask, amount);
// If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != tx.rct_signatures.base.commitments.get(o) {

View File

@@ -31,7 +31,7 @@ use crate::{
ringct::{
generate_key_image,
clsag::{ClsagError, ClsagInput, Clsag},
bulletproofs::{MAX_OUTPUTS, Bulletproofs},
bulletproofs::{MAX_OUTPUTS, Bulletproof},
RctBase, RctPrunable, RctSignatures,
},
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
@@ -39,7 +39,7 @@ use crate::{
wallet::{
address::{Network, AddressSpec, MoneroAddress},
ViewPair, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, uniqueness,
shared_key, commitment_mask, amount_encryption,
shared_key, commitment_mask, compact_amount_encryption,
extra::{ARBITRARY_DATA_MARKER, MAX_ARBITRARY_DATA_SIZE},
},
};
@@ -92,7 +92,7 @@ impl SendOutput {
view_tag,
dest: ((&shared_key * ED25519_BASEPOINT_TABLE) + output.0.spend),
commitment: Commitment::new(commitment_mask(shared_key), output.1),
amount: amount_encryption(output.1, shared_key),
amount: compact_amount_encryption(output.1, shared_key),
},
payment_id,
)
@@ -783,7 +783,11 @@ impl SignableTransaction {
let sum = commitments.iter().map(|commitment| commitment.mask).sum();
// Safe due to the constructor checking MAX_OUTPUTS
let bp = Bulletproofs::prove(rng, &commitments, self.protocol.bp_plus()).unwrap();
let bp = if self.protocol.bp_plus() {
Bulletproof::prove_plus(rng, commitments.clone()).unwrap()
} else {
Bulletproof::prove(rng, &commitments).unwrap()
};
// Create the TX extra
let extra = Self::extra(
@@ -932,7 +936,7 @@ impl Eventuality {
return false;
}
// TODO: Remove this when the following for loop is updated
// TODO: Remove this if/when the following for loop is updated to support older TXs
assert!(
rct_type.compact_encrypted_amounts(),
"created an Eventuality for a very old RctType we don't support proving for"

View File

@@ -36,4 +36,4 @@ dkg = { path = "../../crypto/dkg", default-features = false }
bitcoin-serai = { path = "../../coins/bitcoin", default-features = false, features = ["hazmat"] }
monero-generators = { path = "../../coins/monero/generators", default-features = false }
monero-serai = { path = "../../coins/monero", default-features = false, features = ["cache-distribution"] }
monero-serai = { path = "../../coins/monero", default-features = false }