mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 13:39:25 +00:00
Incomplete work on using Option to remove panic cases
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
49
coins/monero/wallet/README0.md
Normal file
49
coins/monero/wallet/README0.md
Normal 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.
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user