Incomplete work on using Option to remove panic cases

This commit is contained in:
Luke Parker
2024-06-22 09:02:59 -04:00
parent b5b9d4a871
commit 1db40914eb
8 changed files with 255 additions and 165 deletions

View File

@@ -1,49 +1,11 @@
# monero-serai # monero-serai
A modern Monero transaction library intended for usage in wallets. It prides A modern Monero transaction library. It provides a modern, Rust-friendly view of
itself on accuracy, correctness, and removing common pit falls developers may the Monero protocol.
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 ### Purpose and support
monero-serai was written for Serai, a decentralized exchange aiming to 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, Monero. Despite this, monero-serai is intended to be a widely usable library,
accurate to Monero. monero-serai guarantees the functionality needed for Serai, accurate to Monero. monero-serai guarantees the functionality needed for Serai,
yet will not deprive functionality from other users. yet does not include any functionality specific to Serai.
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.

View File

@@ -15,16 +15,26 @@ const CORRECT_BLOCK_HASH_202612: [u8; 32] =
const EXISTING_BLOCK_HASH_202612: [u8; 32] = const EXISTING_BLOCK_HASH_202612: [u8; 32] =
hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"); hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698");
/// A Monero block's header.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct BlockHeader { pub struct BlockHeader {
/// The major version of the protocol, denoting the hard fork.
pub major_version: u8, pub major_version: u8,
/// The minor version of the protocol.
pub minor_version: u8, pub minor_version: u8,
/// Seconds since the epoch.
pub timestamp: u64, pub timestamp: u64,
/// The previous block's hash.
pub previous: [u8; 32], 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, pub nonce: u32,
} }
impl BlockHeader { impl BlockHeader {
/// Write the BlockHeader.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_varint(&self.major_version, w)?; write_varint(&self.major_version, w)?;
write_varint(&self.minor_version, w)?; write_varint(&self.minor_version, w)?;
@@ -33,12 +43,14 @@ impl BlockHeader {
w.write_all(&self.nonce.to_le_bytes()) w.write_all(&self.nonce.to_le_bytes())
} }
/// Serialize the BlockHeader to a Vec<u8>.
pub fn serialize(&self) -> Vec<u8> { pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![]; let mut serialized = vec![];
self.write(&mut serialized).unwrap(); self.write(&mut serialized).unwrap();
serialized serialized
} }
/// Read a BlockHeader.
pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> { pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> {
Ok(BlockHeader { Ok(BlockHeader {
major_version: read_varint(r)?, major_version: read_varint(r)?,
@@ -50,14 +62,22 @@ impl BlockHeader {
} }
} }
/// A Monero block.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct Block { pub struct Block {
/// The block's header.
pub header: BlockHeader, pub header: BlockHeader,
/// The miner's transaction.
pub miner_tx: Transaction, pub miner_tx: Transaction,
/// The transactions within this block.
pub txs: Vec<[u8; 32]>, pub txs: Vec<[u8; 32]>,
} }
impl Block { 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<u64> { pub fn number(&self) -> Option<u64> {
match self.miner_tx.prefix.inputs.first() { match self.miner_tx.prefix.inputs.first() {
Some(Input::Gen(number)) => Some(*number), Some(Input::Gen(number)) => Some(*number),
@@ -65,6 +85,7 @@ impl Block {
} }
} }
/// Write the BlockHeader.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.header.write(w)?; self.header.write(w)?;
self.miner_tx.write(w)?; self.miner_tx.write(w)?;
@@ -75,22 +96,25 @@ impl Block {
Ok(()) Ok(())
} }
fn tx_merkle_root(&self) -> [u8; 32] { /// Serialize the BlockHeader to a Vec<u8>.
merkle_root(self.miner_tx.hash(), &self.txs) pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
} }
/// Serialize the block as required for the proof of work hash. /// 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, /// This is distinct from the serialization required for the block hash. To get the block hash,
/// use the [`Block::hash`] function. /// use the [`Block::hash`] function.
pub fn serialize_hashable(&self) -> Vec<u8> { pub fn serialize_pow_hash(&self) -> Vec<u8> {
let mut blob = self.header.serialize(); 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(); write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap();
blob blob
} }
/// Get the hash of this block.
pub fn hash(&self) -> [u8; 32] { pub fn hash(&self) -> [u8; 32] {
let mut hashable = self.serialize_hashable(); let mut hashable = self.serialize_hashable();
// Monero pre-appends a VarInt of the block hashing blobs length before getting the block hash // 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 { if hash == CORRECT_BLOCK_HASH_202612 {
return EXISTING_BLOCK_HASH_202612; return EXISTING_BLOCK_HASH_202612;
}; };
hash hash
} }
pub fn serialize(&self) -> Vec<u8> { /// Read a BlockHeader.
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> { pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
let header = BlockHeader::read(r)?; let header = BlockHeader::read(r)?;

View File

@@ -15,11 +15,22 @@ pub mod ring_signatures;
/// RingCT structs and functionality. /// RingCT structs and functionality.
pub mod ringct; pub mod ringct;
/// Transaction structs. /// Transaction structs and functionality.
pub mod transaction; pub mod transaction;
/// Block structs. /// Block structs and functionality.
pub mod block; 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; 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; pub const COINBASE_LOCK_WINDOW: usize = 60;
/// Monero's block time target, in seconds.
pub const BLOCK_TIME: usize = 120; pub const BLOCK_TIME: usize = 120;

View File

@@ -10,29 +10,33 @@ use curve25519_dalek::{EdwardsPoint, Scalar};
use crate::{io::*, generators::hash_to_point, primitives::keccak256_to_scalar}; use crate::{io::*, generators::hash_to_point, primitives::keccak256_to_scalar};
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Signature { struct Signature {
c: Scalar, c: Scalar,
r: Scalar, s: Scalar,
} }
impl Signature { impl Signature {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_scalar(&self.c, w)?; write_scalar(&self.c, w)?;
write_scalar(&self.r, w)?; write_scalar(&self.s, w)?;
Ok(()) Ok(())
} }
pub fn read<R: Read>(r: &mut R) -> io::Result<Signature> { fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? }) 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)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct RingSignature { pub struct RingSignature {
sigs: Vec<Signature>, sigs: Vec<Signature>,
} }
impl RingSignature { impl RingSignature {
/// Write the RingSignature.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
for sig in &self.sigs { for sig in &self.sigs {
sig.write(w)?; sig.write(w)?;
@@ -40,31 +44,49 @@ impl RingSignature {
Ok(()) Ok(())
} }
/// Read a RingSignature.
pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> { pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? }) 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 { pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool {
if ring.len() != self.sigs.len() { if ring.len() != self.sigs.len() {
return false; 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); buf.extend_from_slice(msg);
let mut sum = Scalar::ZERO; let mut sum = Scalar::ZERO;
for (ring_member, sig) in ring.iter().zip(&self.sigs) { 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)] #[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()); buf.extend_from_slice(Li.compress().as_bytes());
#[allow(non_snake_case)] #[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()); buf.extend_from_slice(Ri.compress().as_bytes());
sum += sig.c; sum += sig.c;
} }
sum == keccak256_to_scalar(buf) sum == keccak256_to_scalar(buf)
} }
} }

View File

@@ -19,11 +19,6 @@ use crate::{
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof}, 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<Scalar>) -> EdwardsPoint {
hash_to_point((ED25519_BASEPOINT_TABLE * secret.deref()).compress().to_bytes()) * secret.deref()
}
/// An encrypted amount. /// An encrypted amount.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum EncryptedAmount { pub enum EncryptedAmount {
@@ -58,25 +53,35 @@ impl EncryptedAmount {
pub enum RctType { pub enum RctType {
/// No RCT proofs. /// No RCT proofs.
Null, 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, 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, MlsagIndividual,
// One MLSAG for each input and a Bulletproof (RCTTypeBulletproof). // One MLSAG for each input and a Bulletproof.
///
/// This lines up with RCTTypeBulletproof.
Bulletproofs, Bulletproofs,
/// One MLSAG for each input and a Bulletproof, yet starting to use EncryptedAmount::Compact /// One MLSAG for each input and a Bulletproof, yet using EncryptedAmount::Compact.
/// (RCTTypeBulletproof2). ///
/// This lines up with RCTTypeBulletproof2.
BulletproofsCompactAmount, BulletproofsCompactAmount,
/// One CLSAG for each input and a Bulletproof (RCTTypeCLSAG). /// One CLSAG for each input and a Bulletproof.
///
/// This lines up with RCTTypeCLSAG.
Clsag, Clsag,
/// One CLSAG for each input and a Bulletproof+ (RCTTypeBulletproofPlus). /// One CLSAG for each input and a Bulletproof+.
///
/// This lines up with RCTTypeBulletproofPlus.
BulletproofsPlus, BulletproofsPlus,
} }
impl RctType { impl From<RctType> for u8 {
/// Convert the RctType to its byte representation. fn from(kind: RctType) -> u8 {
pub fn to_byte(self) -> u8 { match kind {
match self {
RctType::Null => 0, RctType::Null => 0,
RctType::MlsagAggregate => 1, RctType::MlsagAggregate => 1,
RctType::MlsagIndividual => 2, RctType::MlsagIndividual => 2,
@@ -86,10 +91,12 @@ impl RctType {
RctType::BulletproofsPlus => 6, RctType::BulletproofsPlus => 6,
} }
} }
}
/// Convert the RctType from its byte representation. impl TryFrom<u8> for RctType {
pub fn from_byte(byte: u8) -> Option<Self> { type Error = ();
Some(match byte { fn try_from(byte: u8) -> Result<Self, ()> {
Ok(match byte {
0 => RctType::Null, 0 => RctType::Null,
1 => RctType::MlsagAggregate, 1 => RctType::MlsagAggregate,
2 => RctType::MlsagIndividual, 2 => RctType::MlsagIndividual,
@@ -97,10 +104,12 @@ impl RctType {
4 => RctType::BulletproofsCompactAmount, 4 => RctType::BulletproofsCompactAmount,
5 => RctType::Clsag, 5 => RctType::Clsag,
6 => RctType::BulletproofsPlus, 6 => RctType::BulletproofsPlus,
_ => None?, _ => Err(())?,
}) })
} }
}
impl RctType {
/// Returns true if this RctType uses compact encrypted amounts, false otherwise. /// Returns true if this RctType uses compact encrypted amounts, false otherwise.
pub fn compact_encrypted_amounts(&self) -> bool { pub fn compact_encrypted_amounts(&self) -> bool {
match self { match self {
@@ -125,6 +134,8 @@ pub struct RctBase {
/// The fee used by this transaction. /// The fee used by this transaction.
pub fee: u64, pub fee: u64,
/// The re-randomized amount commitments used within inputs. /// The re-randomized amount commitments used within inputs.
///
/// This field was deprecated and is empty for modern RctTypes.
pub pseudo_outs: Vec<EdwardsPoint>, pub pseudo_outs: Vec<EdwardsPoint>,
/// The encrypted amounts for the recipient to decrypt. /// The encrypted amounts for the recipient to decrypt.
pub encrypted_amounts: Vec<EncryptedAmount>, pub encrypted_amounts: Vec<EncryptedAmount>,
@@ -133,14 +144,15 @@ pub struct RctBase {
} }
impl RctBase { impl RctBase {
/// The weight of this RctBase as relevant for fees.
pub fn fee_weight(outputs: usize, fee: u64) -> usize { pub fn fee_weight(outputs: usize, fee: u64) -> usize {
// 1 byte for the RCT signature type // 1 byte for the RCT signature type
1 + (outputs * (8 + 32)) + varint_len(fee) 1 + (outputs * (8 + 32)) + varint_len(fee)
} }
/// Write the RctBase to a writer. /// Write the RctBase.
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { pub fn write<W: 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 { match rct_type {
RctType::Null => Ok(()), RctType::Null => Ok(()),
_ => { _ => {
@@ -156,10 +168,10 @@ impl RctBase {
} }
} }
/// Read a RctBase from a writer. /// Read a RctBase.
pub fn read<R: Read>(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> { pub fn read<R: Read>(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> {
let rct_type = 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 { match rct_type {
RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {} RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {}
@@ -202,30 +214,27 @@ impl RctBase {
} }
} }
/// The prunable part of the RingCT data.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum RctPrunable { pub enum RctPrunable {
/// Null.
Null, Null,
AggregateMlsagBorromean { /// An aggregate MLSAG with Borromean range proofs.
borromean: Vec<BorromeanRange>, AggregateMlsagBorromean { borromean: Vec<BorromeanRange>, mlsag: Mlsag },
mlsag: Mlsag, /// MLSAGs with Borromean range proofs.
}, MlsagBorromean { borromean: Vec<BorromeanRange>, mlsags: Vec<Mlsag> },
MlsagBorromean { /// MLSAGs with Bulletproofs.
borromean: Vec<BorromeanRange>,
mlsags: Vec<Mlsag>,
},
MlsagBulletproofs { MlsagBulletproofs {
bulletproofs: Bulletproof, bulletproofs: Bulletproof,
mlsags: Vec<Mlsag>, mlsags: Vec<Mlsag>,
pseudo_outs: Vec<EdwardsPoint>, pseudo_outs: Vec<EdwardsPoint>,
}, },
Clsag { /// CLSAGs with Bulletproofs(+).
bulletproofs: Bulletproof, Clsag { bulletproofs: Bulletproof, clsags: Vec<Clsag>, pseudo_outs: Vec<EdwardsPoint> },
clsags: Vec<Clsag>,
pseudo_outs: Vec<EdwardsPoint>,
},
} }
impl RctPrunable { impl RctPrunable {
/// The weight of this RctPrunable as relevant for fees.
#[rustfmt::skip] #[rustfmt::skip]
pub fn fee_weight(bp_plus: bool, ring_len: usize, inputs: usize, outputs: usize) -> usize { 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) // 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)) (inputs * (Clsag::fee_weight(ring_len) + 32))
} }
/// Write the RctPrunable.
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
match self { match self {
RctPrunable::Null => Ok(()), RctPrunable::Null => Ok(()),
@@ -267,12 +277,14 @@ impl RctPrunable {
} }
} }
/// Serialize the RctPrunable to a Vec<u8>.
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> { pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
let mut serialized = vec![]; let mut serialized = vec![];
self.write(&mut serialized, rct_type).unwrap(); self.write(&mut serialized, rct_type).unwrap();
serialized serialized
} }
/// Read a RctPrunable.
pub fn read<R: Read>( pub fn read<R: Read>(
rct_type: RctType, rct_type: RctType,
ring_length: usize, ring_length: usize,
@@ -280,17 +292,6 @@ impl RctPrunable {
outputs: usize, outputs: usize,
r: &mut R, r: &mut R,
) -> io::Result<RctPrunable> { ) -> io::Result<RctPrunable> {
// 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 { Ok(match rct_type {
RctType::Null => RctPrunable::Null, RctType::Null => RctPrunable::Null,
RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean { RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean {
@@ -333,16 +334,21 @@ impl RctPrunable {
}) })
} }
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> { /// Write the RctPrunable as necessary for signing the signature.
match self { ///
RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"), /// This function will return None if the object is `RctPrunable::Null` (and has no
/// representation here).
#[must_use]
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> {
Some(match self {
RctPrunable::Null => None?,
RctPrunable::AggregateMlsagBorromean { borromean, .. } | RctPrunable::AggregateMlsagBorromean { borromean, .. } |
RctPrunable::MlsagBorromean { borromean, .. } => { RctPrunable::MlsagBorromean { borromean, .. } => {
borromean.iter().try_for_each(|rs| rs.write(w)) borromean.iter().try_for_each(|rs| rs.write(w))
} }
RctPrunable::MlsagBulletproofs { bulletproofs, .. } | RctPrunable::MlsagBulletproofs { bulletproofs, .. } |
RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w), RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w),
} })
} }
} }
@@ -354,22 +360,18 @@ pub struct RctSignatures {
impl RctSignatures { impl RctSignatures {
/// RctType for a given RctSignatures struct. /// 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<RctType> {
Some(match &self.prunable {
RctPrunable::Null => RctType::Null, RctPrunable::Null => RctType::Null,
RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate, RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate,
RctPrunable::MlsagBorromean { .. } => RctType::MlsagIndividual, 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 { .. } => { RctPrunable::MlsagBulletproofs { .. } => {
if matches!( if matches!(self.base.encrypted_amounts.first()?, EncryptedAmount::Original { .. }) {
self
.base
.encrypted_amounts
.first()
.expect("MLSAG with Bulletproofs didn't have any outputs"),
EncryptedAmount::Original { .. }
) {
RctType::Bulletproofs RctType::Bulletproofs
} else { } else {
RctType::BulletproofsCompactAmount RctType::BulletproofsCompactAmount
@@ -382,9 +384,10 @@ impl RctSignatures {
RctType::BulletproofsPlus RctType::BulletproofsPlus
} }
} }
} })
} }
/// The weight of this RctSignatures as relevant for fees.
pub fn fee_weight( pub fn fee_weight(
bp_plus: bool, bp_plus: bool,
ring_len: usize, ring_len: usize,
@@ -395,16 +398,20 @@ impl RctSignatures {
RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(bp_plus, ring_len, inputs, outputs) RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(bp_plus, ring_len, inputs, outputs)
} }
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { #[must_use]
let rct_type = self.rct_type(); pub fn write<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> {
self.base.write(w, rct_type)?; let rct_type = self.rct_type()?;
self.prunable.write(w, 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<u8> { #[must_use]
pub fn serialize(&self) -> Option<Vec<u8>> {
let mut serialized = vec![]; let mut serialized = vec![];
self.write(&mut serialized).unwrap(); self.write(&mut serialized)?.unwrap();
serialized Some(serialized)
} }
pub fn read<R: Read>( pub fn read<R: Read>(

View File

@@ -140,6 +140,7 @@ impl Timelock {
if raw == 0 { if raw == 0 {
Timelock::None Timelock::None
} else if raw < 500_000_000 { } else if raw < 500_000_000 {
// TODO: This is trivial to have panic
Timelock::Block(usize::try_from(raw).unwrap()) Timelock::Block(usize::try_from(raw).unwrap())
} else { } else {
Timelock::Time(raw) Timelock::Time(raw)
@@ -150,6 +151,7 @@ impl Timelock {
write_varint( write_varint(
&match self { &match self {
Timelock::None => 0, Timelock::None => 0,
// TODO: Check this unwrap
Timelock::Block(block) => (*block).try_into().unwrap(), Timelock::Block(block) => (*block).try_into().unwrap(),
Timelock::Time(time) => *time, Timelock::Time(time) => *time,
}, },
@@ -268,24 +270,33 @@ impl Transaction {
RctSignatures::fee_weight(bp_plus, ring_len, decoy_weights.len(), outputs, fee) RctSignatures::fee_weight(bp_plus, ring_len, decoy_weights.len(), outputs, fee)
} }
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { #[must_use]
self.prefix.write(w)?; pub fn write<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> {
if let Err(e) = self.prefix.write(w) {
return Some(Err(e));
};
if self.prefix.version == 1 { if self.prefix.version == 1 {
for ring_sig in &self.signatures { 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 { } 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 { } else {
panic!("Serializing a transaction with an unknown version"); Some(Err(io::Error::other("transaction had an unknown version")))
} }
} }
pub fn serialize(&self) -> Vec<u8> { #[must_use]
pub fn serialize(&self) -> Option<Vec<u8>> {
let mut res = Vec::with_capacity(2048); let mut res = Vec::with_capacity(2048);
self.write(&mut res).unwrap(); self.write(&mut res)?.unwrap();
res Some(res)
} }
pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> { pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> {
@@ -352,36 +363,39 @@ impl Transaction {
Ok(Transaction { prefix, signatures, rct_signatures }) 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); let mut buf = Vec::with_capacity(2048);
if self.prefix.version == 1 { if self.prefix.version == 1 {
self.write(&mut buf).unwrap(); self.write(&mut buf)?.unwrap();
keccak256(buf) Some(keccak256(buf))
} else { } else {
let mut hashes = Vec::with_capacity(96); let mut hashes = Vec::with_capacity(96);
hashes.extend(self.prefix.hash()); 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)); hashes.extend(keccak256(&buf));
buf.clear(); buf.clear();
hashes.extend(&match self.rct_signatures.prunable { hashes.extend(&match self.rct_signatures.prunable {
RctPrunable::Null => [0; 32], 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(buf)
} }
}); });
keccak256(hashes) Some(keccak256(hashes))
} }
} }
/// Calculate the hash of this transaction as needed for signing it. /// 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 { if self.prefix.version == 1 {
return self.prefix.hash(); return Some(self.prefix.hash());
} }
let mut buf = Vec::with_capacity(2048); let mut buf = Vec::with_capacity(2048);
@@ -389,18 +403,19 @@ impl Transaction {
sig_hash.extend(self.prefix.hash()); 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)); sig_hash.extend(keccak256(&buf));
buf.clear(); 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)); sig_hash.extend(keccak256(buf));
keccak256(sig_hash) Some(keccak256(sig_hash))
} }
fn is_rct_bulletproof(&self) -> bool { 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::Bulletproofs | RctType::BulletproofsCompactAmount | RctType::Clsag => true,
RctType::Null | RctType::Null |
RctType::MlsagAggregate | RctType::MlsagAggregate |
@@ -410,7 +425,8 @@ impl Transaction {
} }
fn is_rct_bulletproof_plus(&self) -> bool { 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::BulletproofsPlus => true,
RctType::Null | RctType::Null |
RctType::MlsagAggregate | RctType::MlsagAggregate |
@@ -422,15 +438,15 @@ impl Transaction {
} }
/// Calculate the transaction's weight. /// Calculate the transaction's weight.
pub fn weight(&self) -> usize { pub fn weight(&self) -> Option<usize> {
let blob_size = self.serialize().len(); let blob_size = self.serialize()?.len();
let bp = self.is_rct_bulletproof(); let bp = self.is_rct_bulletproof();
let bp_plus = self.is_rct_bulletproof_plus(); let bp_plus = self.is_rct_bulletproof_plus();
if !(bp || bp_plus) { Some(if !(bp || bp_plus) {
blob_size blob_size
} else { } else {
blob_size + Bulletproof::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

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

View File

@@ -27,7 +27,7 @@ use monero_serai::{
io::*, io::*,
primitives::{Commitment, keccak256}, primitives::{Commitment, keccak256},
ringct::{ ringct::{
generate_key_image, hash_to_point,
clsag::{ClsagError, ClsagContext, Clsag}, clsag::{ClsagError, ClsagContext, Clsag},
bulletproofs::{MAX_COMMITMENTS, Bulletproof}, bulletproofs::{MAX_COMMITMENTS, Bulletproof},
RctBase, RctPrunable, RctSignatures, RctBase, RctPrunable, RctSignatures,
@@ -53,6 +53,11 @@ mod multisig;
pub use multisig::TransactionMachine; pub use multisig::TransactionMachine;
use monero_serai::ringct::EncryptedAmount; 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<Scalar>) -> EdwardsPoint {
hash_to_point((ED25519_BASEPOINT_TABLE * secret.deref()).compress().to_bytes()) * secret.deref()
}
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
struct SendOutput { struct SendOutput {