From 1db40914eb039bdab61f7b221866c0de8bb4447d Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 22 Jun 2024 09:02:59 -0400 Subject: [PATCH] Incomplete work on using Option to remove panic cases --- coins/monero/README.md | 44 +------- coins/monero/src/block.rs | 42 +++++--- coins/monero/src/lib.rs | 15 ++- coins/monero/src/ring_signatures.rs | 44 ++++++-- coins/monero/src/ringct.rs | 151 +++++++++++++++------------- coins/monero/src/transaction.rs | 68 ++++++++----- coins/monero/wallet/README0.md | 49 +++++++++ coins/monero/wallet/src/send/mod.rs | 7 +- 8 files changed, 255 insertions(+), 165 deletions(-) create mode 100644 coins/monero/wallet/README0.md diff --git a/coins/monero/README.md b/coins/monero/README.md index 517fb4bb..836154d9 100644 --- a/coins/monero/README.md +++ b/coins/monero/README.md @@ -1,49 +1,11 @@ # monero-serai -A modern Monero transaction library intended for usage in wallets. It prides -itself on accuracy, correctness, and removing common pit falls developers may -face. - -monero-serai also offers the following features: - -- Featured Addresses -- A FROST-based multisig orders of magnitude more performant than Monero's +A modern Monero transaction library. It provides a modern, Rust-friendly view of +the Monero protocol. ### Purpose and support monero-serai was written for Serai, a decentralized exchange aiming to support Monero. Despite this, monero-serai is intended to be a widely usable library, accurate to Monero. monero-serai guarantees the functionality needed for Serai, -yet will not deprive functionality from other users. - -Various legacy transaction formats are not currently implemented, yet we are -willing to add support for them. There aren't active development efforts around -them however. - -### Caveats - -This library DOES attempt to do the following: - -- Create on-chain transactions identical to how wallet2 would (unless told not - to) -- Not be detectable as monero-serai when scanning outputs -- Not reveal spent outputs to the connected RPC node - -This library DOES NOT attempt to do the following: - -- Have identical RPC behavior when creating transactions -- Be a wallet - -This means that monero-serai shouldn't be fingerprintable on-chain. It also -shouldn't be fingerprintable if a targeted attack occurs to detect if the -receiving wallet is monero-serai or wallet2. It also should be generally safe -for usage with remote nodes. - -It won't hide from remote nodes it's monero-serai however, potentially -allowing a remote node to profile you. The implications of this are left to the -user to consider. - -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. +yet does not include any functionality specific to Serai. diff --git a/coins/monero/src/block.rs b/coins/monero/src/block.rs index 832caae5..caa1d74e 100644 --- a/coins/monero/src/block.rs +++ b/coins/monero/src/block.rs @@ -15,16 +15,26 @@ const CORRECT_BLOCK_HASH_202612: [u8; 32] = const EXISTING_BLOCK_HASH_202612: [u8; 32] = hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"); +/// A Monero block's header. #[derive(Clone, PartialEq, Eq, Debug)] pub struct BlockHeader { + /// The major version of the protocol, denoting the hard fork. pub major_version: u8, + /// The minor version of the protocol. pub minor_version: u8, + /// Seconds since the epoch. pub timestamp: u64, + /// The previous block's hash. pub previous: [u8; 32], + /// The nonce used to mine the block. + /// + /// Miners should increment this while attempting to find a block with a hash satisfying the PoW + /// rules. pub nonce: u32, } impl BlockHeader { + /// Write the BlockHeader. pub fn write(&self, w: &mut W) -> io::Result<()> { write_varint(&self.major_version, w)?; write_varint(&self.minor_version, w)?; @@ -33,12 +43,14 @@ impl BlockHeader { w.write_all(&self.nonce.to_le_bytes()) } + /// Serialize the BlockHeader to a Vec. pub fn serialize(&self) -> Vec { let mut serialized = vec![]; self.write(&mut serialized).unwrap(); serialized } + /// Read a BlockHeader. pub fn read(r: &mut R) -> io::Result { Ok(BlockHeader { major_version: read_varint(r)?, @@ -50,14 +62,22 @@ impl BlockHeader { } } +/// A Monero block. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Block { + /// The block's header. pub header: BlockHeader, + /// The miner's transaction. pub miner_tx: Transaction, + /// The transactions within this block. pub txs: Vec<[u8; 32]>, } impl Block { + /// The zero-index position of this block within the blockchain. + /// + /// This information comes from the Block's miner transaction. If the miner transaction isn't + /// structed as expected, this will return None. pub fn number(&self) -> Option { match self.miner_tx.prefix.inputs.first() { Some(Input::Gen(number)) => Some(*number), @@ -65,6 +85,7 @@ impl Block { } } + /// Write the BlockHeader. pub fn write(&self, w: &mut W) -> io::Result<()> { self.header.write(w)?; self.miner_tx.write(w)?; @@ -75,22 +96,25 @@ impl Block { Ok(()) } - fn tx_merkle_root(&self) -> [u8; 32] { - merkle_root(self.miner_tx.hash(), &self.txs) + /// Serialize the BlockHeader to a Vec. + pub fn serialize(&self) -> Vec { + let mut serialized = vec![]; + self.write(&mut serialized).unwrap(); + serialized } /// 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 { + pub fn serialize_pow_hash(&self) -> Vec { let mut blob = self.header.serialize(); - blob.extend_from_slice(&self.tx_merkle_root()); + blob.extend_from_slice(&merkle_root(self.miner_tx.hash(), &self.txs)); write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap(); - blob } + /// Get 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 @@ -103,16 +127,10 @@ impl Block { if hash == CORRECT_BLOCK_HASH_202612 { return EXISTING_BLOCK_HASH_202612; }; - hash } - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - + /// Read a BlockHeader. pub fn read(r: &mut R) -> io::Result { let header = BlockHeader::read(r)?; diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index fc157232..0e0b2fcf 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -15,11 +15,22 @@ pub mod ring_signatures; /// RingCT structs and functionality. pub mod ringct; -/// Transaction structs. +/// Transaction structs and functionality. pub mod transaction; -/// Block structs. +/// Block structs and functionality. pub mod block; +/// The minimum amount of blocks an output is locked for. +/// +/// If Monero suffered a re-organization, any transactions which selected decoys belonging to +/// recent blocks would become invalidated. Accordingly, transactions must use decoys which are +/// presumed to not be invalidated in the future. If wallets only selected n-block-old outputs as +/// decoys, then any ring member within the past n blocks would have to be the real spend. +/// Preventing this at the consensus layer ensures privacy and integrity. pub const DEFAULT_LOCK_WINDOW: usize = 10; + +/// The minimum amount of blocks a coinbase output is locked for. pub const COINBASE_LOCK_WINDOW: usize = 60; + +/// Monero's block time target, in seconds. pub const BLOCK_TIME: usize = 120; diff --git a/coins/monero/src/ring_signatures.rs b/coins/monero/src/ring_signatures.rs index 77528a96..26e721a0 100644 --- a/coins/monero/src/ring_signatures.rs +++ b/coins/monero/src/ring_signatures.rs @@ -10,29 +10,33 @@ use curve25519_dalek::{EdwardsPoint, Scalar}; use crate::{io::*, generators::hash_to_point, primitives::keccak256_to_scalar}; #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] -pub struct Signature { +struct Signature { c: Scalar, - r: Scalar, + s: Scalar, } impl Signature { - pub fn write(&self, w: &mut W) -> io::Result<()> { + fn write(&self, w: &mut W) -> io::Result<()> { write_scalar(&self.c, w)?; - write_scalar(&self.r, w)?; + write_scalar(&self.s, w)?; Ok(()) } - pub fn read(r: &mut R) -> io::Result { - Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? }) + fn read(r: &mut R) -> io::Result { + Ok(Signature { c: read_scalar(r)?, s: read_scalar(r)? }) } } +/// A ring signature. +/// +/// This was used by the original Cryptonote transaction protocol and was deprecated with RingCT. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct RingSignature { sigs: Vec, } impl RingSignature { + /// Write the RingSignature. pub fn write(&self, w: &mut W) -> io::Result<()> { for sig in &self.sigs { sig.write(w)?; @@ -40,31 +44,49 @@ impl RingSignature { Ok(()) } + /// Read a RingSignature. pub fn read(members: usize, r: &mut R) -> io::Result { Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? }) } + /// Verify the ring signature. 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 + (2 * 32 * ring.len())); buf.extend_from_slice(msg); let mut sum = Scalar::ZERO; - for (ring_member, sig) in ring.iter().zip(&self.sigs) { + /* + The traditional Schnorr signature is: + r = sample() + c = H(r G || m) + s = r - c x + Verified as: + s G + c A == R + + Each ring member here performs a dual-Schnorr signature for: + s G + c A + s HtP(A) + c K + Where the transcript is pushed both these values, r G, r HtP(A) for the real spend. + This also serves as a DLEq proof between the key and the key image. + + Checking sum(c) == H(transcript) acts a disjunction, where any one of the `c`s can be + modified to cause the intended sum, if and only if a corresponding `s` value is known. + */ + #[allow(non_snake_case)] - let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.r); + let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.s); 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.s * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image); buf.extend_from_slice(Ri.compress().as_bytes()); sum += sig.c; } - sum == keccak256_to_scalar(buf) } } diff --git a/coins/monero/src/ringct.rs b/coins/monero/src/ringct.rs index 80f17b90..b8a2826c 100644 --- a/coins/monero/src/ringct.rs +++ b/coins/monero/src/ringct.rs @@ -19,11 +19,6 @@ use crate::{ ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof}, }; -/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`. -pub fn generate_key_image(secret: &Zeroizing) -> EdwardsPoint { - hash_to_point((ED25519_BASEPOINT_TABLE * secret.deref()).compress().to_bytes()) * secret.deref() -} - /// An encrypted amount. #[derive(Clone, PartialEq, Eq, Debug)] pub enum EncryptedAmount { @@ -58,25 +53,35 @@ impl EncryptedAmount { pub enum RctType { /// No RCT proofs. Null, - /// One MLSAG for multiple inputs and Borromean range proofs (RCTTypeFull). + /// One MLSAG for multiple inputs and Borromean range proofs. + /// + /// This lines up with RCTTypeFull. MlsagAggregate, - // One MLSAG for each input and a Borromean range proof (RCTTypeSimple). + // One MLSAG for each input and a Borromean range proof. + /// + /// This lines up with RCTTypeSimple. MlsagIndividual, - // One MLSAG for each input and a Bulletproof (RCTTypeBulletproof). + // One MLSAG for each input and a Bulletproof. + /// + /// This lines up with RCTTypeBulletproof. Bulletproofs, - /// One MLSAG for each input and a Bulletproof, yet starting to use EncryptedAmount::Compact - /// (RCTTypeBulletproof2). + /// One MLSAG for each input and a Bulletproof, yet using EncryptedAmount::Compact. + /// + /// This lines up with RCTTypeBulletproof2. BulletproofsCompactAmount, - /// One CLSAG for each input and a Bulletproof (RCTTypeCLSAG). + /// One CLSAG for each input and a Bulletproof. + /// + /// This lines up with RCTTypeCLSAG. Clsag, - /// One CLSAG for each input and a Bulletproof+ (RCTTypeBulletproofPlus). + /// One CLSAG for each input and a Bulletproof+. + /// + /// This lines up with RCTTypeBulletproofPlus. BulletproofsPlus, } -impl RctType { - /// Convert the RctType to its byte representation. - pub fn to_byte(self) -> u8 { - match self { +impl From for u8 { + fn from(kind: RctType) -> u8 { + match kind { RctType::Null => 0, RctType::MlsagAggregate => 1, RctType::MlsagIndividual => 2, @@ -86,10 +91,12 @@ impl RctType { RctType::BulletproofsPlus => 6, } } +} - /// Convert the RctType from its byte representation. - pub fn from_byte(byte: u8) -> Option { - Some(match byte { +impl TryFrom for RctType { + type Error = (); + fn try_from(byte: u8) -> Result { + Ok(match byte { 0 => RctType::Null, 1 => RctType::MlsagAggregate, 2 => RctType::MlsagIndividual, @@ -97,10 +104,12 @@ impl RctType { 4 => RctType::BulletproofsCompactAmount, 5 => RctType::Clsag, 6 => RctType::BulletproofsPlus, - _ => None?, + _ => Err(())?, }) } +} +impl RctType { /// Returns true if this RctType uses compact encrypted amounts, false otherwise. pub fn compact_encrypted_amounts(&self) -> bool { match self { @@ -125,6 +134,8 @@ pub struct RctBase { /// The fee used by this transaction. pub fee: u64, /// The re-randomized amount commitments used within inputs. + /// + /// This field was deprecated and is empty for modern RctTypes. pub pseudo_outs: Vec, /// The encrypted amounts for the recipient to decrypt. pub encrypted_amounts: Vec, @@ -133,14 +144,15 @@ pub struct RctBase { } impl RctBase { + /// The weight of this RctBase as relevant for fees. pub fn fee_weight(outputs: usize, fee: u64) -> usize { // 1 byte for the RCT signature type 1 + (outputs * (8 + 32)) + varint_len(fee) } - /// Write the RctBase to a writer. + /// Write the RctBase. pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { - w.write_all(&[rct_type.to_byte()])?; + w.write_all(&[u8::from(rct_type)])?; match rct_type { RctType::Null => Ok(()), _ => { @@ -156,10 +168,10 @@ impl RctBase { } } - /// Read a RctBase from a writer. + /// Read a RctBase. pub fn 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"))?; + RctType::try_from(read_byte(r)?).map_err(|_| io::Error::other("invalid RCT type"))?; match rct_type { RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {} @@ -202,30 +214,27 @@ impl RctBase { } } +/// The prunable part of the RingCT data. #[derive(Clone, PartialEq, Eq, Debug)] pub enum RctPrunable { + /// Null. Null, - AggregateMlsagBorromean { - borromean: Vec, - mlsag: Mlsag, - }, - MlsagBorromean { - borromean: Vec, - mlsags: Vec, - }, + /// An aggregate MLSAG with Borromean range proofs. + AggregateMlsagBorromean { borromean: Vec, mlsag: Mlsag }, + /// MLSAGs with Borromean range proofs. + MlsagBorromean { borromean: Vec, mlsags: Vec }, + /// MLSAGs with Bulletproofs. MlsagBulletproofs { bulletproofs: Bulletproof, mlsags: Vec, pseudo_outs: Vec, }, - Clsag { - bulletproofs: Bulletproof, - clsags: Vec, - pseudo_outs: Vec, - }, + /// CLSAGs with Bulletproofs(+). + Clsag { bulletproofs: Bulletproof, clsags: Vec, pseudo_outs: Vec }, } impl RctPrunable { + /// The weight of this RctPrunable as relevant for fees. #[rustfmt::skip] pub fn fee_weight(bp_plus: bool, ring_len: usize, inputs: usize, outputs: usize) -> usize { // 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one) @@ -235,6 +244,7 @@ impl RctPrunable { (inputs * (Clsag::fee_weight(ring_len) + 32)) } + /// Write the RctPrunable. pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { match self { RctPrunable::Null => Ok(()), @@ -267,12 +277,14 @@ impl RctPrunable { } } + /// Serialize the RctPrunable to a Vec. pub fn serialize(&self, rct_type: RctType) -> Vec { let mut serialized = vec![]; self.write(&mut serialized, rct_type).unwrap(); serialized } + /// Read a RctPrunable. pub fn read( rct_type: RctType, ring_length: usize, @@ -280,17 +292,6 @@ impl RctPrunable { outputs: usize, r: &mut R, ) -> io::Result { - // While we generally don't bother with misc consensus checks, this affects the safety of - // the below defined rct_type function - // The exact line preventing zero-input transactions is: - // https://github.com/monero-project/monero/blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/ - // src/ringct/rctSigs.cpp#L609 - // And then for RctNull, that's only allowed for miner TXs which require one input of - // Input::Gen - if inputs == 0 { - Err(io::Error::other("transaction had no inputs"))?; - } - Ok(match rct_type { RctType::Null => RctPrunable::Null, RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean { @@ -333,16 +334,21 @@ impl RctPrunable { }) } - pub(crate) fn signature_write(&self, w: &mut W) -> io::Result<()> { - match self { - RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"), + /// Write the RctPrunable as necessary for signing the signature. + /// + /// This function will return None if the object is `RctPrunable::Null` (and has no + /// representation here). + #[must_use] + pub(crate) fn signature_write(&self, w: &mut W) -> Option> { + Some(match self { + RctPrunable::Null => None?, 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), - } + }) } } @@ -354,22 +360,18 @@ pub struct RctSignatures { impl RctSignatures { /// RctType for a given RctSignatures struct. - pub fn rct_type(&self) -> RctType { - match &self.prunable { + /// + /// This is only guaranteed to return the type for a well-formed RctSignatures. For a malformed + /// RctSignatures, this will return either the presumed RctType (with no guarantee of compliance + /// with that type) or None. + #[must_use] + pub fn rct_type(&self) -> Option { + Some(match &self.prunable { RctPrunable::Null => RctType::Null, RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate, RctPrunable::MlsagBorromean { .. } => RctType::MlsagIndividual, - // RctBase ensures there's at least one output, making the following - // inferences guaranteed/expects impossible on any valid RctSignatures RctPrunable::MlsagBulletproofs { .. } => { - if matches!( - self - .base - .encrypted_amounts - .first() - .expect("MLSAG with Bulletproofs didn't have any outputs"), - EncryptedAmount::Original { .. } - ) { + if matches!(self.base.encrypted_amounts.first()?, EncryptedAmount::Original { .. }) { RctType::Bulletproofs } else { RctType::BulletproofsCompactAmount @@ -382,9 +384,10 @@ impl RctSignatures { RctType::BulletproofsPlus } } - } + }) } + /// The weight of this RctSignatures as relevant for fees. pub fn fee_weight( bp_plus: bool, ring_len: usize, @@ -395,16 +398,20 @@ impl RctSignatures { RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(bp_plus, ring_len, inputs, outputs) } - pub fn 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) + #[must_use] + pub fn write(&self, w: &mut W) -> Option> { + let rct_type = self.rct_type()?; + if let Err(e) = self.base.write(w, rct_type) { + return Some(Err(e)); + }; + Some(self.prunable.write(w, rct_type)) } - pub fn serialize(&self) -> Vec { + #[must_use] + pub fn serialize(&self) -> Option> { let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized + self.write(&mut serialized)?.unwrap(); + Some(serialized) } pub fn read( diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index 5e492751..ae194906 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -140,6 +140,7 @@ impl Timelock { if raw == 0 { Timelock::None } else if raw < 500_000_000 { + // TODO: This is trivial to have panic Timelock::Block(usize::try_from(raw).unwrap()) } else { Timelock::Time(raw) @@ -150,6 +151,7 @@ impl Timelock { write_varint( &match self { Timelock::None => 0, + // TODO: Check this unwrap Timelock::Block(block) => (*block).try_into().unwrap(), Timelock::Time(time) => *time, }, @@ -268,24 +270,33 @@ impl Transaction { RctSignatures::fee_weight(bp_plus, ring_len, decoy_weights.len(), outputs, fee) } - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.prefix.write(w)?; + #[must_use] + pub fn write(&self, w: &mut W) -> Option> { + if let Err(e) = self.prefix.write(w) { + return Some(Err(e)); + }; if self.prefix.version == 1 { for ring_sig in &self.signatures { - ring_sig.write(w)?; + if let Err(e) = ring_sig.write(w) { + return Some(Err(e)); + }; } - Ok(()) + Some(Ok(())) } else if self.prefix.version == 2 { - self.rct_signatures.write(w) + if let Err(e) = self.rct_signatures.write(w)? { + return Some(Err(e)); + } + Some(Ok(())) } else { - panic!("Serializing a transaction with an unknown version"); + Some(Err(io::Error::other("transaction had an unknown version"))) } } - pub fn serialize(&self) -> Vec { + #[must_use] + pub fn serialize(&self) -> Option> { let mut res = Vec::with_capacity(2048); - self.write(&mut res).unwrap(); - res + self.write(&mut res)?.unwrap(); + Some(res) } pub fn read(r: &mut R) -> io::Result { @@ -352,36 +363,39 @@ impl Transaction { Ok(Transaction { prefix, signatures, rct_signatures }) } - pub fn hash(&self) -> [u8; 32] { + #[must_use] + pub fn hash(&self) -> Option<[u8; 32]> { let mut buf = Vec::with_capacity(2048); if self.prefix.version == 1 { - self.write(&mut buf).unwrap(); - keccak256(buf) + self.write(&mut buf)?.unwrap(); + Some(keccak256(buf)) } else { let mut hashes = Vec::with_capacity(96); hashes.extend(self.prefix.hash()); - self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); + let rct_type = self.rct_signatures.rct_type()?; + self.rct_signatures.base.write(&mut buf, rct_type).unwrap(); hashes.extend(keccak256(&buf)); buf.clear(); hashes.extend(&match self.rct_signatures.prunable { RctPrunable::Null => [0; 32], _ => { - self.rct_signatures.prunable.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); + self.rct_signatures.prunable.write(&mut buf, rct_type).unwrap(); keccak256(buf) } }); - keccak256(hashes) + Some(keccak256(hashes)) } } /// Calculate the hash of this transaction as needed for signing it. - pub fn signature_hash(&self) -> [u8; 32] { + #[must_use] + pub fn signature_hash(&self) -> Option<[u8; 32]> { if self.prefix.version == 1 { - return self.prefix.hash(); + return Some(self.prefix.hash()); } let mut buf = Vec::with_capacity(2048); @@ -389,18 +403,19 @@ impl Transaction { sig_hash.extend(self.prefix.hash()); - self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); + self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()?).unwrap(); sig_hash.extend(keccak256(&buf)); buf.clear(); - self.rct_signatures.prunable.signature_write(&mut buf).unwrap(); + self.rct_signatures.prunable.signature_write(&mut buf)?.unwrap(); sig_hash.extend(keccak256(buf)); - keccak256(sig_hash) + Some(keccak256(sig_hash)) } fn is_rct_bulletproof(&self) -> bool { - match &self.rct_signatures.rct_type() { + let Some(rct_type) = self.rct_signatures.rct_type() else { return false }; + match rct_type { RctType::Bulletproofs | RctType::BulletproofsCompactAmount | RctType::Clsag => true, RctType::Null | RctType::MlsagAggregate | @@ -410,7 +425,8 @@ impl Transaction { } fn is_rct_bulletproof_plus(&self) -> bool { - match &self.rct_signatures.rct_type() { + let Some(rct_type) = self.rct_signatures.rct_type() else { return false }; + match rct_type { RctType::BulletproofsPlus => true, RctType::Null | RctType::MlsagAggregate | @@ -422,15 +438,15 @@ impl Transaction { } /// Calculate the transaction's weight. - pub fn weight(&self) -> usize { - let blob_size = self.serialize().len(); + pub fn weight(&self) -> Option { + let blob_size = self.serialize()?.len(); let bp = self.is_rct_bulletproof(); let bp_plus = self.is_rct_bulletproof_plus(); - if !(bp || bp_plus) { + Some(if !(bp || bp_plus) { blob_size } else { blob_size + Bulletproof::calculate_bp_clawback(bp_plus, self.prefix.outputs.len()).0 - } + }) } } diff --git a/coins/monero/wallet/README0.md b/coins/monero/wallet/README0.md new file mode 100644 index 00000000..517fb4bb --- /dev/null +++ b/coins/monero/wallet/README0.md @@ -0,0 +1,49 @@ +# monero-serai + +A modern Monero transaction library intended for usage in wallets. It prides +itself on accuracy, correctness, and removing common pit falls developers may +face. + +monero-serai also offers the following features: + +- Featured Addresses +- A FROST-based multisig orders of magnitude more performant than Monero's + +### Purpose and support + +monero-serai was written for Serai, a decentralized exchange aiming to support +Monero. Despite this, monero-serai is intended to be a widely usable library, +accurate to Monero. monero-serai guarantees the functionality needed for Serai, +yet will not deprive functionality from other users. + +Various legacy transaction formats are not currently implemented, yet we are +willing to add support for them. There aren't active development efforts around +them however. + +### Caveats + +This library DOES attempt to do the following: + +- Create on-chain transactions identical to how wallet2 would (unless told not + to) +- Not be detectable as monero-serai when scanning outputs +- Not reveal spent outputs to the connected RPC node + +This library DOES NOT attempt to do the following: + +- Have identical RPC behavior when creating transactions +- Be a wallet + +This means that monero-serai shouldn't be fingerprintable on-chain. It also +shouldn't be fingerprintable if a targeted attack occurs to detect if the +receiving wallet is monero-serai or wallet2. It also should be generally safe +for usage with remote nodes. + +It won't hide from remote nodes it's monero-serai however, potentially +allowing a remote node to profile you. The implications of this are left to the +user to consider. + +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. diff --git a/coins/monero/wallet/src/send/mod.rs b/coins/monero/wallet/src/send/mod.rs index 72a02e02..6ae42e30 100644 --- a/coins/monero/wallet/src/send/mod.rs +++ b/coins/monero/wallet/src/send/mod.rs @@ -27,7 +27,7 @@ use monero_serai::{ io::*, primitives::{Commitment, keccak256}, ringct::{ - generate_key_image, + hash_to_point, clsag::{ClsagError, ClsagContext, Clsag}, bulletproofs::{MAX_COMMITMENTS, Bulletproof}, RctBase, RctPrunable, RctSignatures, @@ -53,6 +53,11 @@ mod multisig; pub use multisig::TransactionMachine; use monero_serai::ringct::EncryptedAmount; +/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`. +pub fn generate_key_image(secret: &Zeroizing) -> EdwardsPoint { + hash_to_point((ED25519_BASEPOINT_TABLE * secret.deref()).compress().to_bytes()) * secret.deref() +} + #[allow(non_snake_case)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] struct SendOutput {