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

View File

@@ -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<W: 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<u8>.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
/// Read a BlockHeader.
pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> {
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<u64> {
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<W: 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<u8>.
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.
///
/// 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<u8> {
pub fn serialize_pow_hash(&self) -> Vec<u8> {
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<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
/// Read a BlockHeader.
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
let header = BlockHeader::read(r)?;

View File

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

View File

@@ -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<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.r, w)?;
write_scalar(&self.s, w)?;
Ok(())
}
pub fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? })
fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
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<Signature>,
}
impl RingSignature {
/// Write the RingSignature.
pub fn write<W: 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<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
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)
}
}

View File

@@ -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<Scalar>) -> 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<RctType> 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<Self> {
Some(match byte {
impl TryFrom<u8> for RctType {
type Error = ();
fn try_from(byte: u8) -> Result<Self, ()> {
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<EdwardsPoint>,
/// The encrypted amounts for the recipient to decrypt.
pub encrypted_amounts: Vec<EncryptedAmount>,
@@ -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<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 {
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)> {
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<BorromeanRange>,
mlsag: Mlsag,
},
MlsagBorromean {
borromean: Vec<BorromeanRange>,
mlsags: Vec<Mlsag>,
},
/// An aggregate MLSAG with Borromean range proofs.
AggregateMlsagBorromean { borromean: Vec<BorromeanRange>, mlsag: Mlsag },
/// MLSAGs with Borromean range proofs.
MlsagBorromean { borromean: Vec<BorromeanRange>, mlsags: Vec<Mlsag> },
/// MLSAGs with Bulletproofs.
MlsagBulletproofs {
bulletproofs: Bulletproof,
mlsags: Vec<Mlsag>,
pseudo_outs: Vec<EdwardsPoint>,
},
Clsag {
bulletproofs: Bulletproof,
clsags: Vec<Clsag>,
pseudo_outs: Vec<EdwardsPoint>,
},
/// CLSAGs with Bulletproofs(+).
Clsag { bulletproofs: Bulletproof, clsags: Vec<Clsag>, pseudo_outs: Vec<EdwardsPoint> },
}
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<W: 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<u8>.
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized, rct_type).unwrap();
serialized
}
/// Read a RctPrunable.
pub fn read<R: Read>(
rct_type: RctType,
ring_length: usize,
@@ -280,17 +292,6 @@ impl RctPrunable {
outputs: usize,
r: &mut R,
) -> 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 {
RctType::Null => RctPrunable::Null,
RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean {
@@ -333,16 +334,21 @@ 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"),
/// 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<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> {
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<RctType> {
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<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)
#[must_use]
pub fn write<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> {
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<u8> {
#[must_use]
pub fn serialize(&self) -> Option<Vec<u8>> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
self.write(&mut serialized)?.unwrap();
Some(serialized)
}
pub fn read<R: Read>(

View File

@@ -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<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.prefix.write(w)?;
#[must_use]
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 {
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<u8> {
#[must_use]
pub fn serialize(&self) -> Option<Vec<u8>> {
let mut res = Vec::with_capacity(2048);
self.write(&mut res).unwrap();
res
self.write(&mut res)?.unwrap();
Some(res)
}
pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> {
@@ -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<usize> {
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
}
})
}
}

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::*,
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<Scalar>) -> 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 {