serai/coins/monero/ringct/clsag/src/lib.rs
Luke Parker a2c3aba82b
Some checks failed
coins/ Tests / test-coins (push) Waiting to run
Coordinator Tests / build (push) Waiting to run
Full Stack Tests / build (push) Waiting to run
Lint / clippy (macos-13) (push) Waiting to run
Lint / clippy (macos-14) (push) Waiting to run
Lint / clippy (ubuntu-latest) (push) Waiting to run
Lint / clippy (windows-latest) (push) Waiting to run
Lint / deny (push) Waiting to run
Lint / fmt (push) Waiting to run
Lint / machete (push) Waiting to run
Monero Tests / unit-tests (push) Waiting to run
Monero Tests / integration-tests (v0.17.3.2) (push) Waiting to run
Monero Tests / integration-tests (v0.18.2.0) (push) Waiting to run
no-std build / build (push) Waiting to run
Processor Tests / build (push) Waiting to run
Reproducible Runtime / build (push) Waiting to run
Tests / test-infra (push) Waiting to run
Tests / test-substrate (push) Waiting to run
Tests / test-serai-client (push) Waiting to run
common/ Tests / test-common (push) Has been cancelled
crypto/ Tests / test-crypto (push) Has been cancelled
Message Queue Tests / build (push) Has been cancelled
Clean the Monero lib for auditing (#577)
* Remove unsafe creation of dalek_ff_group::EdwardsPoint in BP+

* Rename Bulletproofs to Bulletproof, since they are a single Bulletproof

Also bifurcates prove with prove_plus, and adds a few documentation items.

* Make CLSAG signing private

Also adds a bit more documentation and does a bit more tidying.

* 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.

* Tidy Borromean/MLSAG a tad

* Remove experimental feature from monero-serai

* Move amount_decryption into EncryptedAmount::decrypt

* Various RingCT doc comments

* Begin crate smashing

* Further documentation, start shoring up API boundaries of existing crates

* Document and clean clsag

* Add a dedicated send/recv CLSAG mask struct

Abstracts the types used internally.

Also moves the tests from monero-serai to monero-clsag.

* Smash out monero-bulletproofs

Removes usage of dalek-ff-group/multiexp for curve25519-dalek.

Makes compiling in the generators an optional feature.

Adds a structured batch verifier which should be notably more performant.

Documentation and clean up still necessary.

* Correct no-std builds for monero-clsag and monero-bulletproofs

* Tidy and document monero-bulletproofs

I still don't like the impl of the original Bulletproofs...

* Error if missing documentation

* Smash out MLSAG

* Smash out Borromean

* Tidy up monero-serai as a meta crate

* Smash out RPC, wallet

* Document the RPC

* Improve docs a bit

* Move Protocol to monero-wallet

* Incomplete work on using Option to remove panic cases

* Finish documenting monero-serai

* Remove TODO on reading pseudo_outs for AggregateMlsagBorromean

* Only read transactions with one Input::Gen or all Input::ToKey

Also adds a helper to fetch a transaction's prefix.

* Smash out polyseed

* Smash out seed

* Get the repo to compile again

* Smash out Monero addresses

* Document cargo features

Credit to @hinto-janai for adding such sections to their work on documenting
monero-serai in #568.

* Fix deserializing v2 miner transactions

* Rewrite monero-wallet's send code

I have yet to redo the multisig code and the builder. This should be much
cleaner, albeit slower due to redoing work.

This compiles with clippy --all-features. I have to finish the multisig/builder
for --all-targets to work (and start updating the rest of Serai).

* Add SignableTransaction Read/Write

* Restore Monero multisig TX code

* Correct invalid RPC type def in monero-rpc

* Update monero-wallet tests to compile

Some are _consistently_ failing due to the inputs we attempt to spend being too
young. I'm unsure what's up with that. Most seem to pass _consistently_,
implying it's not a random issue yet some configuration/env aspect.

* Clean and document monero-address

* Sync rest of repo with monero-serai changes

* Represent height/block number as a u32

* Diversify ViewPair/Scanner into ViewPair/GuaranteedViewPair and Scanner/GuaranteedScanner

Also cleans the Scanner impl.

* Remove non-small-order view key bound

Guaranteed addresses are in fact guaranteed even with this due to prefixing key
images causing zeroing the ECDH to not zero the shared key.

* Finish documenting monero-serai

* Correct imports for no-std

* Remove possible panic in monero-serai on systems < 32 bits

This was done by requiring the system's usize can represent a certain number.

* Restore the reserialize chain binary

* fmt, machete, GH CI

* Correct misc TODOs in monero-serai

* Have Monero test runner evaluate an Eventuality for all signed TXs

* Fix a pair of bugs in the decoy tests

Unfortunately, this test is still failing.

* Fix remaining bugs in monero-wallet tests

* Reject torsioned spend keys to ensure we can spend the outputs we scan

* Tidy inlined epee code in the RPC

* Correct the accidental swap of stagenet/testnet address bytes

* Remove unused dep from processor

* Handle Monero fee logic properly in the processor

* Document v2 TX/RCT output relation assumed when scanning

* Adjust how we mine the initial blocks due to some CI test failures

* Fix weight estimation for RctType::ClsagBulletproof TXs

* Again increase the amount of blocks we mine prior to running tests

* Correct the if check about when to mine blocks on start

Finally fixes the lack of decoy candidates failures in CI.

* Run Monero on Debian, even for internal testnets

Change made due to a segfault incurred when locally testing.

https://github.com/monero-project/monero/issues/9141 for the upstream.

* Don't attempt running tests on the verify-chain binary

Adds a minimum XMR fee to the processor and runs fmt.

* Increase minimum Monero fee in processor

I'm truly unsure why this is required right now.

* Distinguish fee from necessary_fee in monero-wallet

If there's no change, the fee is difference of the inputs to the outputs. The
prior code wouldn't check that amount is greater than or equal to the necessary
fee, and returning the would-be change amount as the fee isn't necessarily
helpful.

Now the fee is validated in such cases and the necessary fee is returned,
enabling operating off of that.

* Restore minimum Monero fee from develop
2024-07-07 06:57:18 -04:00

400 lines
12 KiB
Rust

#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(non_snake_case)]
use core::ops::Deref;
use std_shims::{
vec,
vec::Vec,
io::{self, Read, Write},
};
use rand_core::{RngCore, CryptoRng};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use subtle::{ConstantTimeEq, ConditionallySelectable};
use curve25519_dalek::{
constants::{ED25519_BASEPOINT_TABLE, ED25519_BASEPOINT_POINT},
scalar::Scalar,
traits::{IsIdentity, MultiscalarMul, VartimePrecomputedMultiscalarMul},
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
};
use monero_io::*;
use monero_generators::hash_to_point;
use monero_primitives::{INV_EIGHT, G_PRECOMP, Commitment, Decoys, keccak256_to_scalar};
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::{ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig};
#[cfg(all(feature = "std", test))]
mod tests;
/// Errors when working with CLSAGs.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum ClsagError {
/// The ring was invalid (such as being too small or too large).
#[cfg_attr(feature = "std", error("invalid ring"))]
InvalidRing,
/// The discrete logarithm of the key, scaling G, wasn't equivalent to the signing ring member.
#[cfg_attr(feature = "std", error("invalid commitment"))]
InvalidKey,
/// 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,
}
/// Context on the input being signed for.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ClsagContext {
// The opening for the commitment of the signing ring member
commitment: Commitment,
// Selected ring members' positions, signer index, and ring
decoys: Decoys,
}
impl ClsagContext {
/// Create a new context, as necessary for signing.
pub fn new(decoys: Decoys, commitment: Commitment) -> Result<ClsagContext, ClsagError> {
if decoys.len() > u8::MAX.into() {
Err(ClsagError::InvalidRing)?;
}
// Validate the commitment matches
if decoys.signer_ring_members()[1] != commitment.calculate() {
Err(ClsagError::InvalidCommitment)?;
}
Ok(ClsagContext { commitment, decoys })
}
}
#[allow(clippy::large_enum_variant)]
enum Mode {
Sign(usize, EdwardsPoint, EdwardsPoint),
Verify(Scalar),
}
// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
//
// Said differences are covered via the above Mode
fn core(
ring: &[[EdwardsPoint; 2]],
I: &EdwardsPoint,
pseudo_out: &EdwardsPoint,
msg: &[u8; 32],
D: &EdwardsPoint,
s: &[Scalar],
A_c1: &Mode,
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
let n = ring.len();
let images_precomp = match A_c1 {
Mode::Sign(..) => None,
Mode::Verify(..) => Some(VartimeEdwardsPrecomputation::new([I, D])),
};
let D_INV_EIGHT = D * INV_EIGHT();
// Generate the transcript
// Instead of generating multiple, a single transcript is created and then edited as needed
const PREFIX: &[u8] = b"CLSAG_";
#[rustfmt::skip]
const AGG_0: &[u8] = b"agg_0";
#[rustfmt::skip]
const ROUND: &[u8] = b"round";
const PREFIX_AGG_0_LEN: usize = PREFIX.len() + AGG_0.len();
let mut to_hash = Vec::with_capacity(((2 * n) + 5) * 32);
to_hash.extend(PREFIX);
to_hash.extend(AGG_0);
to_hash.extend([0; 32 - PREFIX_AGG_0_LEN]);
let mut P = Vec::with_capacity(n);
for member in ring {
P.push(member[0]);
to_hash.extend(member[0].compress().to_bytes());
}
let mut C = Vec::with_capacity(n);
for member in ring {
C.push(member[1] - pseudo_out);
to_hash.extend(member[1].compress().to_bytes());
}
to_hash.extend(I.compress().to_bytes());
to_hash.extend(D_INV_EIGHT.compress().to_bytes());
to_hash.extend(pseudo_out.compress().to_bytes());
// mu_P with agg_0
let mu_P = keccak256_to_scalar(&to_hash);
// mu_C with agg_1
to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
let mu_C = keccak256_to_scalar(&to_hash);
// Truncate it for the round transcript, altering the DST as needed
to_hash.truncate(((2 * n) + 1) * 32);
for i in 0 .. ROUND.len() {
to_hash[PREFIX.len() + i] = ROUND[i];
}
// Unfortunately, it's I D pseudo_out instead of pseudo_out I D, meaning this needs to be
// truncated just to add it back
to_hash.extend(pseudo_out.compress().to_bytes());
to_hash.extend(msg);
// Configure the loop based on if we're signing or verifying
let start;
let end;
let mut c;
match A_c1 {
Mode::Sign(r, A, AH) => {
start = r + 1;
end = r + n;
to_hash.extend(A.compress().to_bytes());
to_hash.extend(AH.compress().to_bytes());
c = keccak256_to_scalar(&to_hash);
}
Mode::Verify(c1) => {
start = 0;
end = n;
c = *c1;
}
}
// Perform the core loop
let mut c1 = c;
for i in (start .. end).map(|i| i % n) {
let c_p = mu_P * c;
let c_c = mu_C * c;
// (s_i * G) + (c_p * P_i) + (c_c * C_i)
let L = match A_c1 {
Mode::Sign(..) => {
EdwardsPoint::multiscalar_mul([s[i], c_p, c_c], [ED25519_BASEPOINT_POINT, P[i], C[i]])
}
Mode::Verify(..) => {
G_PRECOMP().vartime_mixed_multiscalar_mul([s[i]], [c_p, c_c], [P[i], C[i]])
}
};
let PH = hash_to_point(P[i].compress().0);
// (c_p * I) + (c_c * D) + (s_i * PH)
let R = match A_c1 {
Mode::Sign(..) => EdwardsPoint::multiscalar_mul([c_p, c_c, s[i]], [I, D, &PH]),
Mode::Verify(..) => {
images_precomp.as_ref().unwrap().vartime_mixed_multiscalar_mul([c_p, c_c], [s[i]], [PH])
}
};
to_hash.truncate(((2 * n) + 3) * 32);
to_hash.extend(L.compress().to_bytes());
to_hash.extend(R.compress().to_bytes());
c = keccak256_to_scalar(&to_hash);
// This will only execute once and shouldn't need to be constant time. Making it constant time
// removes the risk of branch prediction creating timing differences depending on ring index
// however
c1.conditional_assign(&c, i.ct_eq(&(n - 1)));
}
// This first tuple is needed to continue signing, the latter is the c to be tested/worked with
((D_INV_EIGHT, c * mu_P, c * mu_C), c1)
}
/// The CLSAG signature, as used in Monero.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Clsag {
/// The difference of the commitment randomnesses, scaling the key image generator.
pub D: EdwardsPoint,
/// The responses for each ring member.
pub s: Vec<Scalar>,
/// The first challenge in the ring.
pub c1: Scalar,
}
struct ClsagSignCore {
incomplete_clsag: Clsag,
pseudo_out: EdwardsPoint,
key_challenge: Scalar,
challenged_mask: Scalar,
}
impl Clsag {
// Sign core is the extension of core as needed for signing, yet is shared between single signer
// and multisig, hence why it's still core
fn sign_core<R: RngCore + CryptoRng>(
rng: &mut R,
I: &EdwardsPoint,
input: &ClsagContext,
mask: Scalar,
msg: &[u8; 32],
A: EdwardsPoint,
AH: EdwardsPoint,
) -> ClsagSignCore {
let r: usize = input.decoys.signer_index().into();
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
let mask_delta = input.commitment.mask - mask;
let H = hash_to_point(input.decoys.ring()[r][0].compress().0);
let D = H * mask_delta;
let mut s = Vec::with_capacity(input.decoys.ring().len());
for _ in 0 .. input.decoys.ring().len() {
s.push(Scalar::random(rng));
}
let ((D, c_p, c_c), c1) =
core(input.decoys.ring(), I, &pseudo_out, msg, &D, &s, &Mode::Sign(r, A, AH));
ClsagSignCore {
incomplete_clsag: Clsag { D, s, c1 },
pseudo_out,
key_challenge: c_p,
challenged_mask: c_c * mask_delta,
}
}
/// Sign CLSAG signatures for the provided inputs.
///
/// Monero ensures the rerandomized input commitments have the same value as the outputs by
/// checking `sum(rerandomized_input_commitments) - sum(output_commitments) == 0`. This requires
/// not only the amounts balance, yet also
/// `sum(input_commitment_masks) - sum(output_commitment_masks)`.
///
/// Monero solves this by following the wallet protocol to determine each output commitment's
/// randomness, then using random masks for all but the last input. The last input is
/// rerandomized to the necessary mask for the equation to balance.
///
/// Due to Monero having this behavior, it only makes sense to sign CLSAGs as a list, hence this
/// API being the way it is.
///
/// `inputs` is of the form (discrete logarithm of the key, context).
///
/// `sum_outputs` is for the sum of the output commitments' masks.
pub fn sign<R: RngCore + CryptoRng>(
rng: &mut R,
mut inputs: Vec<(Zeroizing<Scalar>, ClsagContext)>,
sum_outputs: Scalar,
msg: [u8; 32],
) -> Result<Vec<(Clsag, EdwardsPoint)>, ClsagError> {
// Create the key images
let mut key_image_generators = vec![];
let mut key_images = vec![];
for input in &inputs {
let key = input.1.decoys.signer_ring_members()[0];
// Check the key is consistent
if (ED25519_BASEPOINT_TABLE * input.0.deref()) != key {
Err(ClsagError::InvalidKey)?;
}
let key_image_generator = hash_to_point(key.compress().0);
key_image_generators.push(key_image_generator);
key_images.push(key_image_generator * input.0.deref());
}
let mut res = Vec::with_capacity(inputs.len());
let mut sum_pseudo_outs = Scalar::ZERO;
for i in 0 .. inputs.len() {
let mask;
// If this is the last input, set the mask as described above
if i == (inputs.len() - 1) {
mask = sum_outputs - sum_pseudo_outs;
} else {
mask = Scalar::random(rng);
sum_pseudo_outs += mask;
}
let mut nonce = Zeroizing::new(Scalar::random(rng));
let ClsagSignCore { mut incomplete_clsag, pseudo_out, key_challenge, challenged_mask } =
Clsag::sign_core(
rng,
&key_images[i],
&inputs[i].1,
mask,
&msg,
nonce.deref() * ED25519_BASEPOINT_TABLE,
nonce.deref() * key_image_generators[i],
);
// Effectively r - c x, except c x is (c_p x) + (c_c z), where z is the delta between the
// ring member's commitment and our pseudo-out commitment (which will only have a known
// discrete log over G if the amounts cancel out)
incomplete_clsag.s[usize::from(inputs[i].1.decoys.signer_index())] =
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();
debug_assert!(clsag
.verify(inputs[i].1.decoys.ring(), &key_images[i], &pseudo_out, &msg)
.is_ok());
res.push((clsag, pseudo_out));
}
Ok(res)
}
/// Verify a CLSAG signature for the provided context.
pub fn verify(
&self,
ring: &[[EdwardsPoint; 2]],
I: &EdwardsPoint,
pseudo_out: &EdwardsPoint,
msg: &[u8; 32],
) -> Result<(), ClsagError> {
// Preliminary checks
// s, c1, and points must also be encoded canonically, which is checked at time of decode
if ring.is_empty() {
Err(ClsagError::InvalidRing)?;
}
if ring.len() != self.s.len() {
Err(ClsagError::InvalidS)?;
}
if I.is_identity() || (!I.is_torsion_free()) {
Err(ClsagError::InvalidImage)?;
}
let D = self.D.mul_by_cofactor();
if D.is_identity() {
Err(ClsagError::InvalidD)?;
}
let (_, c1) = core(ring, I, pseudo_out, msg, &D, &self.s, &Mode::Verify(self.c1));
if c1 != self.c1 {
Err(ClsagError::InvalidC1)?;
}
Ok(())
}
/// Write a CLSAG.
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.
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)? })
}
}